snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 00:29:55 +05:00
commit 240db4e2a7
15309 changed files with 2173406 additions and 0 deletions

View File

@@ -0,0 +1,169 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @not_cloud @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
let createdCommand;
// Note on selectors: stacked interactive dialogs all render with id `appsModal`.
// cy.get('#appsModal') uses jQuery's getElementById optimization and returns only
// the first match, so the attribute selector `[id="appsModal"]` is used wherever
// BOTH stacked modals must be counted/queried (it falls back to querySelectorAll).
describe('Interactive Dialog - Stacked Dialogs via action_button elements', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.requireWebhookServer();
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for stacked dialogs via action_button elements',
display_name: 'Action Button Dialog Test',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'action_button_dialog',
url: `${webhookBaseUrl}/dialog/action_button_request`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T2560A - Parent dialog renders two distinct action_button elements', () => {
// # Post slash command to open the parent dialog
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the parent apps form modal opens
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify the title is correct
cy.get('#appsModalLabel').should('contain', 'Parent Dialog with Action Button');
cy.get('.modal-body').within(() => {
// * Verify the text field is rendered
cy.contains('Your Name').should('be.visible');
// * Verify BOTH action_button elements render as buttons
cy.contains('button', 'Open Details').should('be.visible');
cy.contains('button', 'Open Summary').should('be.visible');
});
// * Verify the standard submit and cancel buttons are present
cy.get('#appsModalSubmit').should('be.visible');
cy.get('#appsModalCancel').should('be.visible');
});
});
it('MM-T2560B - "Open Details" button stacks a child dialog that reflects the Details source', () => {
// # Intercept the execute API call triggered by the action_button click
cy.intercept('POST', '/api/v4/actions/dialogs/execute').as('executeAction');
// # Open the parent dialog
cy.postMessage(`/${createdCommand.trigger} `);
cy.get('#appsModal').should('be.visible');
cy.get('#appsModalLabel').should('contain', 'Parent Dialog with Action Button');
// # Click the "Open Details" action button
cy.get('#appsModal').within(() => {
cy.contains('button', 'Open Details').click();
});
cy.wait('@executeAction').its('response.statusCode').should('eq', 200);
// * The child dialog reflects WHERE it was pressed: title + introduction.
// Assert the title first — it is the real render synchronizer, so the modal
// count below becomes an instant check instead of the retry driver.
cy.contains('[id="appsModalLabel"]', 'Details Dialog').should('be.visible');
cy.contains('This child dialog was opened from the "Details" action button.').should('be.visible');
// * The child dialog stacks on top while the parent stays open (2 modals)
cy.get('[id="appsModal"]').should('have.length', 2);
// * The Summary child was NOT opened
cy.contains('[id="appsModalLabel"]', 'Summary Dialog').should('not.exist');
// * The parent dialog is still present underneath
cy.contains('[id="appsModalLabel"]', 'Parent Dialog with Action Button').should('exist');
});
it('MM-T2560C - "Open Summary" button stacks a child dialog that reflects the Summary source', () => {
// # Intercept the execute API call
cy.intercept('POST', '/api/v4/actions/dialogs/execute').as('executeAction');
// # Open the parent dialog
cy.postMessage(`/${createdCommand.trigger} `);
cy.get('#appsModal').should('be.visible');
// # Click the "Open Summary" action button
cy.get('#appsModal').within(() => {
cy.contains('button', 'Open Summary').click();
});
cy.wait('@executeAction').its('response.statusCode').should('eq', 200);
// * The child dialog reflects the Summary source — assert the title first as
// the render synchronizer, proving the two buttons open distinct child dialogs.
cy.contains('[id="appsModalLabel"]', 'Summary Dialog').should('be.visible');
cy.contains('This child dialog was opened from the "Summary" action button.').should('be.visible');
// * The child dialog stacks on top (2 modals)
cy.get('[id="appsModal"]').should('have.length', 2);
// * The Details child was NOT opened
cy.contains('[id="appsModalLabel"]', 'Details Dialog').should('not.exist');
// * The parent dialog is still present underneath
cy.contains('[id="appsModalLabel"]', 'Parent Dialog with Action Button').should('exist');
});
it('MM-T2560D - Submitting the child dialog returns to the parent dialog', () => {
// # Intercept the execute API call
cy.intercept('POST', '/api/v4/actions/dialogs/execute').as('executeAction');
// # Open the parent dialog and click an action button
cy.postMessage(`/${createdCommand.trigger} `);
cy.get('#appsModal').should('be.visible');
cy.get('#appsModal').within(() => {
cy.contains('button', 'Open Details').click();
});
cy.wait('@executeAction').its('response.statusCode').should('eq', 200);
// * Wait for the child to render, then confirm both dialogs are stacked
cy.contains('[id="appsModalLabel"]', 'Details Dialog').should('be.visible');
cy.get('[id="appsModal"]').should('have.length', 2);
// # Submit the child dialog (target its modal via the verified title)
cy.contains('[id="appsModalLabel"]', 'Details Dialog').closest('[id="appsModal"]').within(() => {
cy.get('#appsModalSubmit').click();
});
// * The child dialog is dismissed — only the parent remains
cy.get('[id="appsModal"]').should('have.length', 1);
cy.get('#appsModalLabel').should('contain', 'Parent Dialog with Action Button');
});
});

View File

@@ -0,0 +1,108 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
const webhookUtils = require('../../../../utils/webhook_utils');
let createdCommand;
let simpleDialog;
describe('Interactive Dialog - Apps Form', () => {
before(() => {
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
cy.requireWebhookServer();
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for boolean dialog',
display_name: 'Simple Dialog with boolean element',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'boolean_dialog',
url: `${webhookBaseUrl}/boolean_dialog_request`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
simpleDialog = webhookUtils.getBooleanDialog(createdCommand.id, webhookBaseUrl);
});
});
});
it('MM-T2502 - Boolean element check', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify that the header of modal contains icon URL, title and close button
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.get('#appsModalIconUrl').should('be.visible').and('have.attr', 'src').and('not.be.empty');
cy.get('#appsModalLabel').should('be.visible').and('have.text', simpleDialog.dialog.title);
cy.wrap($elForm).find('button.close').should('be.visible').and('contain', '×').and('contain', 'Close');
});
// * Verify that the body contains the boolean element
cy.get('.modal-body').should('be.visible').children('.form-group').should('have.length', 1).each(($elForm, index) => {
const element = simpleDialog.dialog.elements[index];
expect(element.name).to.equal('boolean_input');
cy.wrap($elForm).within(() => {
// Verify the label text includes display name
cy.get('label').first().scrollIntoView().should('be.visible').and('contain', element.display_name);
// Verify the checkbox structure
cy.get('.checkbox').should('be.visible').within(() => {
cy.get('input[type=\'checkbox\']').
should('be.visible').
and('be.checked');
cy.get('span').should('have.text', element.placeholder);
});
// Verify help text if present
if (element.help_text) {
cy.get('.help-text').should('be.visible').and('contain', element.help_text);
}
});
});
// * Verify that the footer contains cancel and submit buttons
cy.get('.modal-footer').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('#appsModalCancel').should('be.visible').and('have.text', 'Cancel');
cy.wrap($elForm).find('#appsModalSubmit').should('be.visible').and('have.text', simpleDialog.dialog.submit_label);
});
closeAppsFormModal();
});
});
});
function closeAppsFormModal() {
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
}

View File

@@ -0,0 +1,177 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Stage: @prod
// Group: @channels @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*
* Single slash command `/checkbox_group_dialog` opens the full feature demo dialog:
* - two checkbox_group fields (label before / after)
* - optional radio with clear
* - bool with label before
* - two checkbox_matrix fields (multiple / single row selection)
*/
const webhookUtils = require('../../../../utils/webhook_utils');
let checkboxGroupCommand;
function openCheckboxDemoDialog() {
cy.postMessage(`/${checkboxGroupCommand.trigger} `);
cy.get('#appsModal').should('be.visible');
}
describe('Interactive Dialog - Checkbox Elements', () => {
before(() => {
cy.apiSaveTeammateNameDisplayPreference('username');
cy.requireWebhookServer();
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const groupCommand = {
auto_complete: false,
description: 'Checkbox dialog feature demo',
display_name: 'Checkbox Dialog Demo',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'checkbox_group_dialog',
url: `${webhookBaseUrl}/checkbox_group_dialog_request`,
username: '',
};
cy.apiCreateCommand(groupCommand).then(({data}) => {
checkboxGroupCommand = data;
webhookUtils.getCheckboxGroupDialog(data.id, webhookBaseUrl);
});
});
});
afterEach(() => {
// # Reload the page after each test to close any dialog left open, so the
// next test's cy.postMessage isn't blocked by a modal covering the textbox.
cy.reload();
});
it('MM-T9001 - checkbox_group renders label before and label after', () => {
openCheckboxDemoDialog();
cy.get('#appsModal').within(() => {
cy.get('.modal-body').within(() => {
cy.get('[data-testid="waiver_group_label_before"]').within(() => {
cy.get('label.control-label').should('contain', 'Group A: label before');
cy.get('.checkbox').should('have.length', 3);
cy.get('.checkbox').each(($checkbox) => {
cy.wrap($checkbox).find('label').then(($label) => {
const textNode = $label.find('.inline-choice-setting__text')[0];
const inputNode = $label.find('input[type="checkbox"]')[0];
expect(textNode.compareDocumentPosition(inputNode)).to.equal(Node.DOCUMENT_POSITION_FOLLOWING);
});
});
cy.get('input[type="checkbox"]').eq(0).should('be.checked');
cy.get('input[type="checkbox"]').eq(1).should('not.be.checked');
cy.get('input[type="checkbox"]').eq(2).should('be.checked');
});
cy.get('[data-testid="waiver_group_label_after"]').within(() => {
cy.get('label.control-label').should('contain', 'Group B: label after');
cy.get('.checkbox').should('have.length', 3);
cy.get('.checkbox').each(($checkbox) => {
cy.wrap($checkbox).find('label').then(($label) => {
const textNode = $label.find('.inline-choice-setting__text')[0];
const inputNode = $label.find('input[type="checkbox"]')[0];
expect(textNode.compareDocumentPosition(inputNode)).to.equal(Node.DOCUMENT_POSITION_PRECEDING);
});
});
cy.get('input[type="checkbox"]').eq(1).should('be.checked');
});
});
});
});
it('MM-T9002 - checkbox_matrix multiple toggles checkboxes per row', () => {
openCheckboxDemoDialog();
cy.get('#appsModal').within(() => {
cy.get('[data-testid="severity_multiple"]').within(() => {
// The demo dialog is taller than the modal body; scroll the matrix
// into view before asserting visibility (be.visible does not auto-scroll).
cy.get('table.checkbox-matrix').scrollIntoView().should('be.visible');
cy.get('table.checkbox-matrix tbody tr').should('have.length', 3);
cy.get('table.checkbox-matrix thead th').should('have.length', 3);
cy.get('input[type="checkbox"]').should('have.length', 6);
cy.get('table.checkbox-matrix tbody tr').first().within(() => {
cy.get('input[type="checkbox"]').first().check();
cy.get('input[type="checkbox"]').last().check();
cy.get('input[type="checkbox"]').first().should('be.checked');
cy.get('input[type="checkbox"]').last().should('be.checked');
});
});
});
});
it('MM-T9003 - checkbox_matrix single uses radio inputs per row', () => {
openCheckboxDemoDialog();
cy.get('#appsModal').within(() => {
cy.get('[data-testid="priority_single"]').within(() => {
cy.get('table.checkbox-matrix tbody tr').should('have.length', 3);
cy.get('input[type="radio"]').should('have.length', 9);
cy.get('table.checkbox-matrix tbody tr').first().within(() => {
cy.get('input[type="radio"]').eq(1).check();
cy.get('input[type="radio"]').eq(1).should('be.checked');
cy.get('input[type="radio"]').eq(0).should('not.be.checked');
cy.get('input[type="radio"]').eq(2).should('not.be.checked');
});
});
});
});
it('MM-T9004 - optional radio shows clear selection control', () => {
openCheckboxDemoDialog();
cy.get('#appsModal').within(() => {
cy.get('[data-testid="department"]').within(() => {
cy.get('input[type="radio"][value="sales"]').should('be.checked');
cy.get('.radio-setting__clear').scrollIntoView().should('be.visible').click();
cy.get('input[type="radio"]:checked').should('not.exist');
});
});
});
it('MM-T9005 - required checkbox_group blocks submit when empty', () => {
openCheckboxDemoDialog();
cy.get('#appsModal').within(() => {
cy.get('[data-testid="waiver_group_label_before"]').within(() => {
cy.get('input[type="checkbox"]').uncheck({force: true});
});
cy.get('#appsModalSubmit').click();
cy.get('[data-testid="waiver_group_label_before"]').find('.error-text').should('be.visible');
});
});
it('MM-T9006 - bool and optional checkbox_group render on demo dialog', () => {
openCheckboxDemoDialog();
cy.get('#appsModal').within(() => {
cy.get('[data-testid="acknowledged"]').within(() => {
cy.get('label.control-label').should('contain', 'Acknowledgement');
cy.get('.inline-choice-setting__text').should('contain', 'I understand the waiver terms');
cy.get('input[type="checkbox"]').should('exist');
});
cy.get('[data-testid="waiver_group_label_after"]').within(() => {
cy.get('label.control-label').should('contain', '(optional)');
});
});
});
});

View File

@@ -0,0 +1,567 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
let createdCommand;
describe('Interactive Dialog - Date and DateTime Fields', () => {
let testTeam;
let testChannel;
// Helper functions to reduce code duplication
const openDateTimeDialog = (command = '') => {
cy.postMessage(`/${createdCommand.trigger} ${command}`);
cy.get('#appsModal').should('be.visible');
};
const openDatePicker = (formGroupName) => {
cy.get('#appsModal').within(() => {
cy.contains('.form-group', formGroupName).within(() => {
cy.get('.date-time-input').click();
});
});
// Calendar should be visible with modal-overflow class automatically applied
cy.get('.rdp', {timeout: 5000}).should('be.visible');
};
// Helper to compute a day safely selectable in the date picker.
// Uses an offset from today to avoid midnight boundary issues.
// Returns {day: string, needsNextMonth: boolean}
const getSelectableDay = (daysFromToday = 2) => {
const now = new Date();
const today = now.getDate();
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
const targetDay = today + daysFromToday;
if (targetDay <= lastDayOfMonth) {
return {day: targetDay.toString(), needsNextMonth: false};
}
return {day: (targetDay - lastDayOfMonth).toString(), needsNextMonth: true};
};
const selectDateFromPicker = ({day, needsNextMonth = false}) => {
if (needsNextMonth) {
cy.get('.rdp .rdp-nav_button_next').click();
}
cy.get('.rdp').find('.rdp-day:not(.rdp-day_outside)').filter((i, el) => el.textContent.trim() === day).first().click();
};
const verifyModalTitle = (title) => {
cy.get('#appsModal').within(() => {
cy.get('#appsModalLabel').should('be.visible').and('contain', title);
});
};
const verifyFormGroup = (groupName, options = {}) => {
const selector = options.scrollIntoView ? cy.contains('.form-group', groupName).scrollIntoView().should('be.visible') : cy.contains('.form-group', groupName).should('be.visible');
return selector.within(() => {
if (options.label) {
cy.get('label').should('contain', options.label);
}
if (options.helpText) {
cy.get('.help-text').should('contain', options.helpText);
}
if (options.inputSelector) {
cy.get(options.inputSelector).should('be.visible');
}
});
};
before(() => {
cy.requireWebhookServer();
// # Use apiInitSetup like the working boolean test
cy.apiInitSetup().then(({team, channel}) => {
testTeam = team;
testChannel = channel;
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for datetime dialog',
display_name: 'DateTime Dialog',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'datetime_dialog',
url: `${webhookBaseUrl}/datetime_dialog_request`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
// # Visit the test channel to ensure we're in the right place
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
});
});
});
beforeEach(() => {
// # Ensure we're in the test channel before each test
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
// # Wait for the channel to be fully loaded
cy.get('#postListContent').should('be.visible');
cy.get('#post_textbox').should('be.visible');
});
it('MM-T2530A - Date field UI and basic functionality', () => {
// # Open datetime dialog and verify modal
openDateTimeDialog('basic');
verifyModalTitle('DateTime Fields Test');
// * Verify the Event Date form group
verifyFormGroup('Event Date', {
label: 'Event Date',
helpText: 'Select the date for your event',
inputSelector: '.date-time-input',
});
// # Open date picker and select a date
openDatePicker('Event Date');
selectDateFromPicker(getSelectableDay());
// * Verify the selected date appears in the field
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Event Date').within(() => {
cy.get('.date-time-input__value').should('be.visible').and('not.be.empty');
});
});
});
it('MM-T2530B - DateTime field UI and functionality', () => {
// # Open datetime dialog
openDateTimeDialog();
// * Verify the Meeting Time datetime field
verifyFormGroup('Meeting Time', {
label: 'Meeting Time',
helpText: 'Select the date and time for your meeting',
inputSelector: '.apps-form-datetime-input',
});
cy.get('#appsModal').within(() => {
// * Verify datetime input structure
cy.contains('.form-group', 'Meeting Time').within(() => {
cy.get('.apps-form-datetime-input').within(() => {
cy.get('.dateTime').should('be.visible');
cy.get('.dateTime__date').should('be.visible');
cy.get('.dateTime__time').should('be.visible');
});
});
// # Open date picker and select date
cy.contains('.form-group', 'Meeting Time').within(() => {
cy.get('.dateTime__date .date-time-input').click();
});
});
cy.get('.rdp', {timeout: 5000}).should('be.visible');
selectDateFromPicker(getSelectableDay(3));
// # Open time menu and select time
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Meeting Time').within(() => {
cy.get('.dateTime__time button[data-testid="time_button"]').click();
});
});
cy.get('[id="expiryTimeMenu"]', {timeout: 10000}).should('be.visible');
cy.get('[id^="time_option_"]').first().click();
});
it('MM-T2530C - Date field validation with min_date constraint', () => {
// # Open datetime dialog
openDateTimeDialog('mindate');
// * Verify the Future Date Only field with min_date constraint
verifyFormGroup('Future Date Only', {
label: 'Future Date Only',
helpText: 'Must be today or later',
});
// # Open the date picker for constrained field
openDatePicker('Future Date Only');
// * Verify a past date is disabled (use 2 days ago to avoid midnight boundary)
// Navigate to previous month if the past date is in a different month
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 2);
const needsPrevMonth = pastDate.getMonth() !== new Date().getMonth();
if (needsPrevMonth) {
cy.get('.rdp .rdp-nav_button_previous').click();
}
const pastDay = pastDate.getDate().toString();
cy.get('.rdp').find('.rdp-day:not(.rdp-day_outside)').
filter((i, el) => el.textContent.trim() === pastDay).
should('have.class', 'rdp-day_disabled').and('be.disabled');
// * Verify a future date is enabled and select it (use +2 days for midnight safety)
const {day: futureDay, needsNextMonth} = getSelectableDay(2);
if (needsPrevMonth) {
// Return to current month after validating a past date in previous month
cy.get('.rdp .rdp-nav_button_next').click();
}
if (needsNextMonth) {
cy.get('.rdp .rdp-nav_button_next').click();
}
cy.get('.rdp').find('.rdp-day:not(.rdp-day_outside)').
filter((i, el) => el.textContent.trim() === futureDay).
should('not.have.class', 'rdp-day_disabled').and('not.be.disabled').
first().click();
// * Verify date selection
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Future Date Only').within(() => {
cy.get('.date-time-input__value').should('be.visible').and('not.be.empty');
});
});
});
it('MM-T2530D - DateTime field with custom time interval', () => {
// # Open datetime dialog with interval command
cy.get('#postListContent').should('be.visible');
openDateTimeDialog('interval');
// * Verify custom interval datetime field
verifyFormGroup('Custom Interval Time', {
inputSelector: '.apps-form-datetime-input',
scrollIntoView: true,
});
// # Open time menu
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Custom Interval Time').within(() => {
cy.get('.dateTime__time button[data-testid="time_button"]').click();
});
});
// * Verify time menu with custom intervals
cy.get('[id="expiryTimeMenu"]', {timeout: 10000}).should('be.visible');
cy.get('[id^="time_option_"]').should('have.length.greaterThan', 0);
});
it('MM-T2530E - Form submission with date and datetime values', () => {
// # Open dialog and select date
openDateTimeDialog('basic');
openDatePicker('Event Date');
selectDateFromPicker(getSelectableDay());
// # Submit the form
cy.get('#appsModal').within(() => {
cy.get('#appsModalSubmit').click();
});
// * Verify submission success
cy.get('#appsModal', {timeout: 10000}).should('not.exist');
cy.get('#postListContent', {timeout: 10000}).within(() => {
cy.get('.post-message__text').last().should('contain', 'Form submitted successfully');
});
});
it('MM-T2530F - Relative date values functionality', () => {
// # Open datetime dialog
openDateTimeDialog('relative');
// * Verify relative date field with 'today' default is pre-populated
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Relative Date Example').scrollIntoView().should('be.visible').within(() => {
cy.get('.date-time-input__value').should('be.visible').and('not.be.empty');
});
// * Verify relative datetime field with '+1d' default is pre-populated
cy.contains('.form-group', 'Relative DateTime Example').scrollIntoView().should('be.visible').within(() => {
cy.get('.apps-form-datetime-input').should('not.be.empty');
});
});
});
it('MM-T2530G - Date field locale formatting', () => {
// # Set browser locale to en-US for consistent formatting
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`, {
onBeforeLoad(win) {
Object.defineProperty(win.navigator, 'language', {value: 'en-US'});
Object.defineProperty(win.navigator, 'languages', {value: ['en-US', 'en']});
},
});
// # Open dialog, select date, and verify locale formatting
openDateTimeDialog('basic');
openDatePicker('Event Date');
const selectedDay = getSelectableDay();
selectDateFromPicker(selectedDay);
// * Verify en-US locale formatting (e.g., "Aug 10, 2025")
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Event Date').within(() => {
cy.get('.date-time-input__value').should('be.visible').and('not.be.empty').invoke('text').then((text) => {
const match = text.trim().match(/^[A-Z][a-z]{2} (\d{1,2}), \d{4}$/);
expect(match, 'date format').to.not.be.null;
expect(Number(match[1]), 'selected day').to.equal(Number(selectedDay.day));
});
});
});
});
it('MM-T2530H - DateTime field respects 12h/24h time preference', () => {
// # Set user preference to 24-hour time
cy.apiSaveClockDisplayModeTo24HourPreference(true);
cy.reload();
cy.get('#postListContent').should('be.visible');
// # Open datetime dialog
openDateTimeDialog();
// * Verify Meeting Time field
verifyFormGroup('Meeting Time', {
inputSelector: '.apps-form-datetime-input',
});
// # Select a date
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Meeting Time').within(() => {
cy.get('.dateTime__date .date-time-input').click();
});
});
cy.get('.rdp', {timeout: 5000}).should('be.visible');
selectDateFromPicker(getSelectableDay());
// # Open time menu
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Meeting Time').within(() => {
cy.get('.dateTime__time button[data-testid="time_button"]').click();
});
});
// * Verify 24-hour format in dropdown (e.g., "14:00" not "2:00 PM")
cy.get('[id="expiryTimeMenu"]', {timeout: 10000}).should('be.visible');
cy.get('[id^="time_option_"]').first().invoke('text').then((text) => {
expect(text).to.match(/^\d{2}:\d{2}$/); // 24-hour format: HH:MM
});
// # Select a time
cy.get('[id^="time_option_"]').eq(5).click();
// * Verify selected time shows in 24-hour format
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Meeting Time').within(() => {
cy.get('.dateTime__time .date-time-input__value').invoke('text').then((text) => {
expect(text).to.match(/^\d{2}:\d{2}$/);
});
});
});
// # Close dialog
cy.get('#appsModal').within(() => {
cy.get('#appsModalCancel').click();
});
// # Set user preference to 12-hour time
cy.apiSaveClockDisplayModeTo24HourPreference(false);
cy.reload();
cy.get('#postListContent').should('be.visible');
// # Open dialog again
openDateTimeDialog();
// # Select date and open time menu
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Meeting Time').within(() => {
cy.get('.dateTime__date .date-time-input').click();
});
});
cy.get('.rdp').should('be.visible');
selectDateFromPicker(getSelectableDay(3));
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Meeting Time').within(() => {
cy.get('.dateTime__time button[data-testid="time_button"]').click();
});
});
// * Verify 12-hour format in dropdown (e.g., "2:00 PM" not "14:00")
cy.get('[id="expiryTimeMenu"]').should('be.visible');
cy.get('[id^="time_option_"]').first().invoke('text').then((text) => {
expect(text).to.match(/\d{1,2}:\d{2} [AP]M/); // 12-hour format: H:MM AM/PM
});
});
it('MM-T2530O - Manual time entry (basic functionality)', () => {
// # Open timezone-manual dialog via webhook
openDateTimeDialog('timezone-manual');
verifyModalTitle('Timezone & Manual Entry Demo');
// * Verify local manual entry field exists
verifyFormGroup('Your Local Time (Manual Entry)', {
helpText: 'Type any time',
});
// # Type a time in manual entry field
cy.get('#appsModal').within(() => {
cy.contains('.form-group', 'Your Local Time (Manual Entry)').within(() => {
cy.get('input#time_input').should('be.visible').type('3:45pm').blur();
});
});
// * Verify time is accepted (no error state)
cy.contains('.form-group', 'Your Local Time (Manual Entry)').within(() => {
cy.get('input#time_input').should('not.have.class', 'error');
cy.get('input#time_input').should('have.value', '3:45 PM');
});
// # Submit form
cy.get('#appsModal').within(() => {
cy.get('#appsModalSubmit').click();
});
// * Verify submission success
cy.get('#appsModal', {timeout: 10000}).should('not.exist');
});
it('MM-T2530P - Manual time entry (multiple formats)', () => {
openDateTimeDialog('timezone-manual');
const testFormats = [
{input: '12a', expected12h: '12:00 AM'},
{input: '14:30', expected12h: '2:30 PM'},
{input: '9pm', expected12h: '9:00 PM'},
];
testFormats.forEach(({input, expected12h}) => {
cy.contains('.form-group', 'Your Local Time (Manual Entry)').within(() => {
cy.get('input#time_input').clear().type(input).blur();
// Wait for formatting to apply
cy.wait(100);
// Verify time is formatted correctly (assumes 12h preference for test consistency)
cy.get('input#time_input').invoke('val').should('equal', expected12h);
});
});
});
it('MM-T2530Q - Manual time entry (invalid format)', () => {
openDateTimeDialog('timezone-manual');
// # Type invalid time
cy.contains('.form-group', 'Your Local Time (Manual Entry)').within(() => {
cy.get('input#time_input').type('abc').blur();
// * Verify error state
cy.get('input#time_input').should('have.class', 'error');
});
// # Type valid time
cy.contains('.form-group', 'Your Local Time (Manual Entry)').within(() => {
cy.get('input#time_input').clear().type('2:30pm').blur();
// * Verify error clears
cy.get('input#time_input').should('not.have.class', 'error');
});
});
it('MM-T2530R - Timezone support (dropdown)', function() {
// Skip if running in London timezone (can't test timezone conversion)
const userTimezone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
if (userTimezone === 'Europe/London' || userTimezone === 'GMT' || userTimezone.includes('London')) {
this.skip();
}
openDateTimeDialog('timezone-manual');
// * Verify timezone indicator is shown
cy.contains('.form-group', 'London Office Hours (Dropdown)').within(() => {
cy.contains('Times in GMT').should('be.visible');
});
// # Select a date
cy.contains('.form-group', 'London Office Hours (Dropdown)').within(() => {
cy.get('.dateTime__date .date-time-input').click();
});
cy.get('.rdp').should('be.visible');
selectDateFromPicker(getSelectableDay());
// # Open time dropdown
cy.contains('.form-group', 'London Office Hours (Dropdown)').within(() => {
cy.get('.dateTime__time button[data-testid="time_button"]').click();
});
// * Verify dropdown shows times starting at midnight (London time)
cy.get('[id="expiryTimeMenu"]').should('be.visible');
cy.get('[id^="time_option_"]').first().invoke('text').then((text) => {
// Should show midnight in 12h or 24h format
expect(text).to.match(/^(12:00 AM|00:00)$/);
});
// # Select a time
cy.get('[id^="time_option_"]').eq(5).click();
// # Submit form
cy.get('#appsModal').within(() => {
cy.get('#appsModalSubmit').click();
});
// * Verify submission success (UTC conversion verified server-side)
cy.get('#appsModal', {timeout: 10000}).should('not.exist');
});
it('MM-T2530S - Timezone support (manual entry)', function() {
// Skip if running in London timezone
const userTimezone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
if (userTimezone === 'Europe/London' || userTimezone === 'GMT' || userTimezone.includes('London')) {
this.skip();
}
openDateTimeDialog('timezone-manual');
// * Verify timezone indicator is shown
cy.contains('.form-group', 'London Office Hours (Manual Entry)').within(() => {
cy.contains('Times in GMT').should('be.visible');
});
// # Select date
cy.contains('.form-group', 'London Office Hours (Manual Entry)').within(() => {
cy.get('.dateTime__date .date-time-input').click();
});
cy.get('.rdp').should('be.visible');
selectDateFromPicker(getSelectableDay());
// # Type time in manual entry
cy.contains('.form-group', 'London Office Hours (Manual Entry)').within(() => {
cy.get('input#time_input').clear().type('2:30pm').blur();
// * Verify time is accepted
cy.get('input#time_input').should('not.have.class', 'error');
});
// # Submit form
cy.get('#appsModal').within(() => {
cy.get('#appsModalSubmit').click();
});
// * Verify submission success (timezone conversion happens server-side)
cy.get('#appsModal', {timeout: 10000}).should('not.exist');
});
});

View File

@@ -0,0 +1,271 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Group: @channels @interactive_dialog @plugin
// If the contents of the interactive dialog from the demo plugin change, please update the demoPluginDialogElements object.
import {demoPlugin} from '@/utils/plugins';
describe('Interactive Dialogs', () => {
let testTeam;
let testChannel;
// # The format of the elements in the interactive dialog from the demo plugin:
const demoPluginDialogElements = [
{
display_name: 'Display Name',
name: 'realname',
type: 'text',
default: 'default text',
placeholder: 'placeholder',
help_text: 'This a regular input in an interactive dialog triggered by a test integration.',
}, {
display_name: 'Email',
name: 'someemail',
type: 'text',
subtype: 'email',
placeholder: 'placeholder@bladekick.com',
help_text: 'This a regular email input in an interactive dialog triggered by a test integration.',
}, {
display_name: 'Password',
name: 'somepassword',
type: 'text',
subtype: 'password',
placeholder: 'Password',
help_text: 'This a password input in an interactive dialog triggered by a test integration.',
}, {
display_name: 'Number',
name: 'somenumber',
type: 'text',
subtype: 'number',
}, {
display_name: 'Display Name Long Text Area',
name: 'realnametextarea',
type: 'textarea',
placeholder: 'placeholder',
optional: true,
min_length: 5,
max_length: 100,
}, {
display_name: 'User Selector',
name: 'someuserselector',
type: 'select',
placeholder: 'Select a user...',
help_text: 'Choose a user from the list.',
optional: true,
min_length: 5,
max_length: 100,
data_source: 'users',
}, {
display_name: 'Channel Selector',
name: 'somechannelselector',
type: 'select',
placeholder: 'Select a channel...',
help_text: 'Choose a channel from the list.',
optional: true,
min_length: 5,
max_length: 100,
data_source: 'channels',
}, {
display_name: 'Option Selector',
name: 'someoptionselector',
type: 'select',
placeholder: 'Select an option...',
help_text: 'Choose an option from the list.',
options: [{
text: 'Option1',
value: 'opt1',
}, {
text: 'Option2',
value: 'opt2',
}, {
text: 'Option3',
value: 'opt3',
}],
}, {
display_name: 'Option Selector with default',
name: 'someoptionselector2',
type: 'select',
default: 'opt2',
placeholder: 'Select an option...',
help_text: 'Choose an option from the list.',
options: [{
text: 'Option1',
value: 'opt1',
}, {
text: 'Option2',
value: 'opt2',
}, {
text: 'Option3',
value: 'opt3',
}],
}, {
display_name: 'Boolean Selector',
name: 'someboolean',
type: 'bool',
placeholder: 'Agree to the terms of service',
help_text: 'You must agree to the terms of service to proceed.',
}, {
display_name: 'Boolean Selector',
name: 'someboolean_optional',
type: 'bool',
placeholder: 'Sign up for monthly emails?',
help_text: "It's up to you if you want to get monthly emails.",
optional: true,
}, {
display_name: 'Boolean Selector (default true)',
name: 'someboolean_default_true',
type: 'bool',
placeholder: 'Enable secure login',
help_text: 'You must enable secure login to proceed.',
default: 'true',
}, {
display_name: 'Boolean Selector (default true)',
name: 'someboolean_default_true_optional',
type: 'bool',
placeholder: 'Enable painfully secure login',
help_text: 'You may optionally enable painfully secure login.',
default: 'true',
optional: true,
}, {
display_name: 'Boolean Selector (default false)',
name: 'someboolean_default_false',
type: 'bool',
placeholder: 'Agree to the annoying terms of service',
help_text: 'You must also agree to the annoying terms of service to proceed.',
default: 'false',
}, {
display_name: 'Boolean Selector (default false)',
name: 'someboolean_default_false_optional',
type: 'bool',
placeholder: 'Throw-away account',
help_text: 'A throw-away account will be deleted after 24 hours.',
default: 'false',
optional: true,
}, {
display_name: 'Radio Option Selector',
name: 'someradiooptionselector',
type: 'radio',
help_text: 'Choose an option from the list.',
options: [{
text: 'Option1',
value: 'opt1',
}, {
text: 'Option2',
value: 'opt2',
}, {
text: 'Option3',
value: 'opt3',
}],
},
];
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.shouldHavePluginUploadEnabled();
// # Set plugin settings
const newSettings = {
PluginSettings: {
Enable: true,
},
};
cy.apiUpdateConfig(newSettings);
cy.apiInitSetup().then(({team, channel}) => {
testTeam = team;
testChannel = channel;
// # Install demo plugin by file
cy.apiUploadAndEnablePlugin(demoPlugin);
// # Visit the test channel
cy.visit(`/${testTeam.name}/channels/${testChannel.name}`);
});
});
it('MM-T2503 Boolean value check', () => {
// # Post the /dialog slash command message
cy.uiGetPostTextBox().type('/dialog {enter}');
// * Check that the apps form modal from the plugin opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify that the body is visible
cy.get('.modal-body').should('be.visible').children('.form-group').each(($elForm, index) => {
const element = demoPluginDialogElements[index];
// Skip if element is undefined (more DOM elements than expected)
if (!element) {
return;
}
// * Verify that when the element comes into view the proper display name is showing
cy.wrap($elForm).find('label').first().scrollIntoView().should('be.visible').and('contain', element.display_name);
if (element.name.includes('someboolean')) {
// * Verify that the checkbox for a boolean element is visible
cy.wrap($elForm).find('.checkbox').should('be.visible').within(() => {
// * Verify that if the element default is true, it should be checked, and vice versa.
let checked = true;
if (element.default === 'true') {
cy.get(`#${element.name}`).
should('be.visible').
and('be.checked');
} else {
cy.get(`#${element.name}`).
should('be.visible').
not('be.checked');
checked = false;
}
// * Verify that the checkbox has the proper text.
cy.get('span').should('have.text', element.placeholder);
// # Click on the checkbox.
if (checked === false) {
cy.get(`#${element.name}`).check();
}
});
} else if (element.name === 'someemail') {
cy.get(`#${element.name}`).scrollIntoView().clear().type('test@test.com');
} else if (element.name === 'somepassword') {
cy.get(`#${element.name}`).scrollIntoView().clear().type('test');
} else if (element.name === 'somenumber') {
cy.get(`#${element.name}`).scrollIntoView().clear().type('42');
} else if (element.name === 'someoptionselector') {
// Click the dropdown
cy.wrap($elForm).find('[id^=\'MultiInput_\']').click();
// Break out of the .within() scope to find options that are portaled to body
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').first().click();
});
} else if (element.name === 'someradiooptionselector') {
cy.wrap($elForm).find('input').first().click();
}
});
// # Submit the form of the interactive dialog.
cy.intercept('/api/v4/actions/dialogs/submit').as('submitAction');
cy.get('#appsModalSubmit').click();
});
// * The apps form modal should not be visible anymore.
cy.get('#appsModal').should('not.exist');
// * Verify that submitted values are boolean and not strings.
cy.wait('@submitAction').should('include.all.keys', ['request', 'response']).then((result) => {
const boolQuestions = Object.entries(result.request.body.submission).filter((element) => element[0].includes('someboolean'));
for (let i = 0; i < boolQuestions.length; i++) {
assert.isBoolean(boolQuestions[i][1]);
}
});
});
});

View File

@@ -0,0 +1,372 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @not_cloud @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
import * as TIMEOUTS from '@/fixtures/timeouts';
const webhookUtils = require('../../../../utils/webhook_utils');
let createdCommand;
let dynamicSelectDialog;
describe('Interactive Dialog - Dynamic Select', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.requireWebhookServer();
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for dynamic select dialog elements',
display_name: 'Dynamic Select Dialog Test',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'dynamic_select_dialog',
url: `${webhookBaseUrl}/dynamic_select_dialog_request`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
dynamicSelectDialog = webhookUtils.getDynamicSelectDialog(createdCommand.id, webhookBaseUrl);
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T2520A - Dynamic select UI and structure verification', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify that the header contains correct title
cy.get('.modal-header').should('be.visible').within(() => {
cy.get('#appsModalLabel').should('be.visible').and('have.text', dynamicSelectDialog.dialog.title);
cy.get('#appsModalIconUrl').should('be.visible').and('have.attr', 'src').and('not.be.empty');
cy.get('button.close').should('be.visible').and('contain', '×').and('contain', 'Close');
});
// * Verify that the body contains both dynamic select elements
cy.get('.modal-body').should('be.visible').children('.form-group').should('have.length', 2).each(($elForm, index) => {
const element = dynamicSelectDialog.dialog.elements[index];
if (!element) {
return;
}
cy.wrap($elForm).within(() => {
// Verify the label text includes display name
cy.get('label').first().scrollIntoView().should('be.visible').and('contain', element.display_name);
if (element.name === 'dynamic_role_selector') {
// * Verify required dynamic select field starts empty
cy.get('[id^=\'MultiInput_\']').should('be.visible');
cy.get('.react-select__single-value').should('not.exist');
cy.get('.react-select__placeholder').should('contain', element.placeholder);
} else if (element.name === 'optional_dynamic_selector') {
// * Verify optional dynamic select field is visible (may have default value)
cy.get('[id^=\'MultiInput_\']').should('be.visible');
// Note: Default values for dynamic selects may not be resolved until user interaction
// The field may show the raw value or be empty initially
}
// * Verify help text if present
if (element.help_text) {
cy.get('.help-text').should('be.visible').and('contain', element.help_text);
}
});
});
// * Verify that the footer contains cancel and submit buttons
cy.get('.modal-footer').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('#appsModalCancel').should('be.visible').and('have.text', 'Cancel');
cy.wrap($elForm).find('#appsModalSubmit').should('be.visible').and('have.text', dynamicSelectDialog.dialog.submit_label);
});
closeAppsFormModal();
});
});
it('MM-T2520B - Dynamic select search functionality', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Test dynamic search in the required field
cy.get('.form-group').eq(0).within(() => {
// # Click to open dropdown and verify initial options load
cy.get('[id^=\'MultiInput_\']').click();
cy.wait(TIMEOUTS.HALF_SEC); // Wait for dynamic options to load
// * Verify dropdown opens with initial options
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').should('have.length.at.least', 1);
cy.wrap(doc).find('.react-select__option').first().should('contain', 'Backend Engineer');
});
// # Test search filtering by typing directly in the control
cy.get('.react-select__control').click().type('frontend');
cy.wait(TIMEOUTS.ONE_SEC); // Wait longer for search results
// * Verify filtered results contain 'frontend' matches
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').should('have.length.at.least', 1);
cy.wrap(doc).find('.react-select__option').each(($option) => {
cy.wrap($option).should('contain.text', 'Frontend');
});
});
// # Select an option
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Frontend Engineer').click();
});
// * Verify selection was made
cy.get('.react-select__single-value').should('contain', 'Frontend Engineer');
});
closeAppsFormModal();
});
});
it('MM-T2520C - Dynamic select with different search terms', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Test different search scenarios
cy.get('.form-group').eq(0).within(() => {
// # Test search for "manager"
cy.get('[id^=\'MultiInput_\']').click().type('manager');
cy.wait(TIMEOUTS.HALF_SEC); // Wait for search results
// * Verify manager-related results
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').should('have.length.at.least', 1);
cy.wrap(doc).find('.react-select__option').each(($option) => {
cy.wrap($option).invoke('text').should('match', /manager/i);
});
});
// # Clear and test search for "senior"
cy.get('[id^=\'MultiInput_\']').click().type('senior');
cy.wait(TIMEOUTS.HALF_SEC); // Wait for search results
// * Verify senior-related results
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').should('have.length.at.least', 1);
cy.wrap(doc).find('.react-select__option').each(($option) => {
cy.wrap($option).invoke('text').should('match', /senior/i);
});
});
// # Test search with no matches
cy.get('[id^=\'MultiInput_\']').type('xyz123nomatch');
cy.wait(TIMEOUTS.HALF_SEC); // Wait for search results
// * Verify no options when no matches
cy.document().then((doc) => {
// Either no options exist or "No options" message is shown
cy.wrap(doc).find('.react-select__menu').then(($menu) => {
if ($menu.length > 0) {
cy.wrap(doc).find('.react-select__option, .react-select__menu-notice--no-options').should('exist');
}
});
});
cy.wait(TIMEOUTS.HALF_SEC); // Wait for default options to load
// # Clear search to get back to default options
cy.get('[id^=\'MultiInput_\']').click();
cy.wait(TIMEOUTS.HALF_SEC); // Wait for default options to load
// # Select a valid option for form submission test
cy.get('[id^=\'MultiInput_\']').click();
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').first().click();
});
});
closeAppsFormModal();
});
});
it('MM-T2521A - Dynamic select form submission', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Select value in required field
cy.get('.form-group').eq(0).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.wait(TIMEOUTS.HALF_SEC); // Wait for options to load
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('DevOps Engineer').click();
});
// # Modify the optional field (which has a default)
cy.get('.form-group').eq(1).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.wait(TIMEOUTS.HALF_SEC); // Wait for options to load
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('QA Engineer').click();
});
// # Submit the form
cy.intercept('POST', '/api/v4/actions/dialogs/submit').as('submitAction');
cy.get('#appsModalSubmit').click();
});
// * Verify that the apps form modal is closed
cy.get('#appsModal').should('not.exist');
// * Verify that submitted values are correct
cy.wait('@submitAction').should('include.all.keys', ['request', 'response']).then((result) => {
const {submission} = result.request.body;
// * Verify dynamic select fields submitted with correct values
expect(submission.dynamic_role_selector).to.equal('devops_eng');
expect(submission.optional_dynamic_selector).to.equal('qa_eng');
});
// * Verify success message
cy.getLastPost().should('contain', 'Dialog submitted');
});
it('MM-T2521B - Dynamic select validation error handling', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Clear the required field (first field starts empty, second has default)
cy.get('.form-group').eq(0).within(() => {
// Field should already be empty, but verify
cy.get('.react-select__single-value').should('not.exist');
});
// # Try to submit the form with empty required field
cy.get('#appsModalSubmit').click();
cy.wait(TIMEOUTS.HALF_SEC); // Wait for potential validation to appear
});
// * Verify that the apps form modal is still open (validation failed)
cy.get('#appsModal').should('be.visible');
// * Verify error message appears for required field
cy.get('#appsModal').within(() => {
cy.get('.form-group').eq(0).within(() => {
cy.get('.error-text').should('be.visible').and('contain', 'This field is required');
});
});
closeAppsFormModal();
});
it('MM-T2522 - Dynamic select keyboard navigation', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Test keyboard navigation in dynamic select
cy.get('.form-group').eq(0).within(() => {
// # Open dropdown and navigate with keyboard
cy.get('[id^=\'MultiInput_\']').click();
cy.wait(TIMEOUTS.HALF_SEC); // Wait for options to load
// # Navigate using arrow keys
cy.get('[id^=\'MultiInput_\']').type('{downarrow}{downarrow}{enter}');
// * Verify selection was made (should be the third option in default list)
cy.get('.react-select__single-value').should('exist').and('not.be.empty');
});
closeAppsFormModal();
});
});
it('MM-T2523 - Dynamic select accessibility check', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Test accessibility elements for dynamic select
cy.get('.form-group').eq(0).within(() => {
// * Verify label exists and is visible
cy.get('label').should('be.visible').and('contain', 'Dynamic Role Selector');
// * Verify dynamic select input is present and accessible
cy.get('[id^=\'MultiInput_\']').should('be.visible');
// * Verify help text exists and is visible
cy.get('.help-text').should('be.visible').and('contain', 'Start typing to search');
// * Test basic interaction accessibility - click to open/close
cy.get('[id^=\'MultiInput_\']').click();
cy.wait(TIMEOUTS.HALF_SEC); // Wait for options to load
// * Verify dropdown opens (options become available)
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').should('have.length.at.least', 1);
});
});
// * Close dropdown by clicking elsewhere in the modal
cy.get('.modal-body').click({force: true});
});
closeAppsFormModal();
});
});
function closeAppsFormModal() {
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
}

View File

@@ -0,0 +1,221 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @not_cloud @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
import * as TIMEOUTS from '@/fixtures/timeouts';
let createdCommand;
describe('Interactive Dialog - Field Refresh', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.requireWebhookServer();
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for field refresh functionality',
display_name: 'Field Refresh Dialog Test',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'field_refresh_dialog',
url: `${webhookBaseUrl}/dialog/field-refresh`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T2540A - Field refresh changes form content within same modal', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify initial state - fields are visible in correct order
cy.get('#appsModalLabel').should('contain', 'Field Refresh Demo'); // Modal title stays same during refresh
cy.get('.modal-body').within(() => {
// * Verify initial field refresh dialog fields are present in new order
cy.contains('Project Name').should('be.visible');
cy.contains('Project Type').should('be.visible');
cy.get('.form-group').should('have.length', 2); // project_name + project_type
});
// # Enter project name first
cy.get('input[placeholder*="project name"]').type('Web App Project');
// # Trigger field refresh by changing project type (refresh: true field)
cy.get('.form-group').contains('Project Type').parent().within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.wait(TIMEOUTS.HALF_SEC);
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Web Application').click();
});
// * Wait for field refresh to complete
cy.wait(TIMEOUTS.ONE_SEC);
// * Verify modal title stays the same (field refresh, not form submission)
cy.get('#appsModalLabel').should('contain', 'Field Refresh Demo');
// * Verify form has dynamic fields based on project type
cy.get('.modal-body').within(() => {
// * Basic fields should still be visible in correct order
cy.contains('Project Name').should('be.visible');
cy.contains('Project Type').should('be.visible');
// * Project name value should be preserved
cy.get('input[placeholder*="project name"]').should('have.value', 'Web App Project');
// * Selection should be made
cy.get('.react-select__single-value').should('contain', 'Web Application');
// * New framework field should appear for web application
cy.contains('Framework').should('be.visible');
cy.get('.form-group').should('have.length', 3); // project_name + project_type + framework
});
closeAppsFormModal();
});
});
it('MM-T2540B - Field values preserved during refresh and form submits successfully', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
cy.get('#appsModal').should('be.visible').within(() => {
// # Fill in initial values
cy.get('input[placeholder*="project name"]').type('My Test Project');
// # Trigger refresh by selecting project type
cy.get('.form-group').contains('Project Type').parent().within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.wait(TIMEOUTS.HALF_SEC);
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Mobile App').click();
});
cy.wait(TIMEOUTS.ONE_SEC);
// * Verify project name value is preserved during refresh
cy.get('input[placeholder*="project name"]').should('have.value', 'My Test Project');
// * Verify project type selection is preserved
cy.get('.react-select__single-value').should('contain', 'Mobile App');
// * Verify platform field appeared for mobile app
cy.contains('Platform').should('be.visible');
// # Fill in the platform field and submit
cy.get('.form-group').contains('Platform').parent().within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.wait(TIMEOUTS.HALF_SEC);
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('React Native').click();
});
// # Submit the form
cy.get('.modal-footer button').contains('Submit').click();
});
// * Verify form was submitted successfully with preserved values
cy.get('.post__body').should('contain', 'Field refresh dialog submitted successfully!');
cy.get('.post__body').should('contain', 'My Test Project');
cy.get('.post__body').should('contain', 'mobile');
cy.get('.post__body').should('contain', 'react-native');
});
it('MM-T2540C - Multiple refresh cycles work correctly', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
cy.get('#appsModal').should('be.visible').within(() => {
// # Enter project name first
cy.get('input[placeholder*="project name"]').type('Multi-Test Project');
const projectTypes = [
'Web Application',
'Mobile App',
'API Service',
'Web Application',
];
projectTypes.forEach((projectType) => {
// # Select different project type
cy.get('.form-group').contains('Project Type').parent().within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.wait(TIMEOUTS.HALF_SEC);
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains(projectType).click();
});
cy.wait(TIMEOUTS.ONE_SEC);
// * Verify basic fields are always present in correct order
cy.contains('Project Name').should('be.visible');
cy.contains('Project Type').should('be.visible');
// * Verify project name is preserved through refresh cycles
cy.get('input[placeholder*="project name"]').should('have.value', 'Multi-Test Project');
// * Verify selection was made
cy.get('.react-select__single-value').should('contain', projectType);
// * Verify project-specific fields appear dynamically
if (projectType === 'Web Application') {
cy.contains('Framework').should('be.visible');
cy.get('.form-group').should('have.length', 3);
} else if (projectType === 'Mobile App') {
cy.contains('Platform').should('be.visible');
cy.get('.form-group').should('have.length', 3);
} else if (projectType === 'API Service') {
cy.contains('Language').should('be.visible');
cy.get('.form-group').should('have.length', 3);
}
});
closeAppsFormModal();
});
});
});
function closeAppsFormModal() {
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
}

View File

@@ -0,0 +1,243 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @not_cloud @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
const webhookUtils = require('../../../../utils/webhook_utils');
let createdCommand;
let fileUploadDialog;
describe('Interactive Dialog - File Upload', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.requireWebhookServer();
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
// # Create slash command that triggers the file upload dialog
const command = {
auto_complete: false,
description: 'Test for file upload dialog elements',
display_name: 'File Upload Dialog Test',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'file_upload_dialog',
url: `${webhookBaseUrl}/file_upload_dialog_request`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
fileUploadDialog = webhookUtils.getFileUploadDialog(createdCommand.id, webhookBaseUrl);
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T6070 - Renders file upload dialog with correct labels and buttons', () => {
// # Post the slash command to open the dialog
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify header contains the correct title
cy.get('.modal-header').should('be.visible').within(() => {
cy.get('#appsModalLabel').should('be.visible').and('have.text', fileUploadDialog.dialog.title);
});
// * Verify both file form-groups are present with correct display names
cy.get('.modal-body').should('be.visible').within(() => {
const singleElement = fileUploadDialog.dialog.elements[0];
const multipleElement = fileUploadDialog.dialog.elements[1];
// * Verify single_document form-group renders with its label
cy.get('.apps-form-file-upload').eq(0).within(() => {
cy.get('label').should('be.visible').and('contain', singleElement.display_name);
// * Verify "Choose File" button for allow_multiple=false
cy.get('button.btn-tertiary').should('be.visible').and('have.text', 'Choose File');
// * Verify placeholder / help text is visible before any upload
cy.get('.help-text').should('be.visible');
});
// * Verify multiple_files form-group renders with its label
cy.get('.apps-form-file-upload').eq(1).within(() => {
cy.get('label').should('be.visible').and('contain', multipleElement.display_name);
// * Verify "Choose Files" button for allow_multiple=true
cy.get('button.btn-tertiary').should('be.visible').and('have.text', 'Choose Files');
// * Verify placeholder / help text is visible before any upload
cy.get('.help-text').should('be.visible');
});
});
// * Verify footer submit label matches the dialog definition
cy.get('.modal-footer').should('be.visible').within(($footer) => {
cy.wrap($footer).find('#appsModalCancel').should('be.visible').and('have.text', 'Cancel');
cy.wrap($footer).find('#appsModalSubmit').should('be.visible').and('have.text', fileUploadDialog.dialog.submit_label);
});
});
// # Close the modal (outside within() so the not.exist check is not vacuous)
closeAppsFormModal();
});
it('MM-T6071 - Uploads files to both required fields and submits successfully', () => {
// # Post the slash command to open the dialog
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Upload a file to the single_document field (required, allow_multiple=false)
cy.get('input#single_document').attachFile('png-image-file.png');
// * Verify a completed file preview appears for single_document
// (.post-image__details renders only once the upload has finished)
cy.get('.apps-form-file-upload').eq(0).within(() => {
cy.get('.post-image__details').should('have.length', 1);
});
// # Upload a file to the multiple_files field (required, allow_multiple=true)
cy.get('input#multiple_files').attachFile('small-image.png');
// * Verify a completed file preview appears for multiple_files
cy.get('.apps-form-file-upload').eq(1).within(() => {
cy.get('.post-image__details').should('have.length', 1);
});
// # Wait for the submit button to re-enable. It is disabled while any field
// is interacting (an upload in progress) and re-enables only once uploads
// settle — the same point the uploaded file IDs propagate into the form
// values. Deterministic signal, no fixed wait needed.
cy.get('#appsModalSubmit').should('not.be.disabled');
// # Intercept the dialog submit API call and click Submit
cy.intercept('/api/v4/actions/dialogs/submit').as('submitAction');
cy.get('#appsModalSubmit').click();
});
// * Verify that the apps form modal is closed after successful submission
cy.get('#appsModal').should('not.exist');
// * Verify the submission body contains the correct file ID structure
cy.wait('@submitAction').should('include.all.keys', ['request', 'response']).then((result) => {
const {submission} = result.request.body;
// * Verify single_document is submitted as a string (single file ID)
expect(submission.single_document).to.be.a('string').and.have.length.greaterThan(0);
// * Verify multiple_files is submitted as a string (comma-separated IDs)
expect(submission.multiple_files).to.be.a('string').and.have.length.greaterThan(0);
// * Verify file_ids is an array containing all uploaded file IDs
expect(result.request.body.file_ids).to.be.an('array').and.have.length.greaterThan(0);
const singleId = submission.single_document;
const multipleIds = submission.multiple_files.split(',').filter(Boolean);
// * Verify file_ids contains all the IDs from both fields
expect(result.request.body.file_ids).to.include.members([singleId, ...multipleIds]);
});
// * Verify the success post appears in the channel
cy.getLastPost().should('contain', 'Dialog submitted successfully!');
});
it('MM-T6072 - allow_multiple appends files; single replaces on second selection', () => {
// # Post the slash command to open the dialog
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Test allow_multiple=true appends: attach first file to multiple_files
cy.get('input#multiple_files').attachFile('png-image-file.png');
// * Verify one completed preview item appears
cy.get('.apps-form-file-upload').eq(1).within(() => {
cy.get('.post-image__details').should('have.length', 1);
});
// # Attach a second file to multiple_files
cy.get('input#multiple_files').attachFile('small-image.png');
// * Verify two preview items appear (appended, not replaced)
cy.get('.apps-form-file-upload').eq(1).within(() => {
cy.get('.post-image__details').should('have.length', 2);
});
// # Test allow_multiple=false replaces: attach a file to single_document
cy.get('input#single_document').attachFile('png-image-file.png');
// * Verify the first file completed (one preview item)
cy.get('.apps-form-file-upload').eq(0).within(() => {
cy.get('.post-image__details').should('have.length', 1);
});
// # Attach a different file to single_document (should replace, not append)
cy.get('input#single_document').attachFile('small-image.png');
// * Verify only one preview item exists for single_document (replaced)
cy.get('.apps-form-file-upload').eq(0).within(() => {
cy.get('.post-image__details').should('have.length', 1);
});
});
closeAppsFormModal();
});
it('MM-T6073 - Required validation fires when submitting with no files selected', () => {
// # Post the slash command to open the dialog
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Submit without uploading any files (both file fields are required).
// Assert the button is interactive first — no uploads are in progress so it
// is enabled immediately; this is a deterministic gate, not a fixed wait.
cy.get('#appsModalSubmit').should('be.enabled').click();
});
// * Verify that the apps form modal is still visible (validation blocked submission)
cy.get('#appsModal').should('be.visible');
// * Verify the inline required error renders on the file field itself,
// consistent with every other apps-form field type (rendered via .error-text)
cy.get('#appsModal').within(() => {
cy.get('.apps-form-file-upload').eq(0).within(() => {
cy.get('.error-text').should('be.visible').and('contain', 'This field is required');
});
});
closeAppsFormModal();
});
});
function closeAppsFormModal() {
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
}

View File

@@ -0,0 +1,314 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
const webhookUtils = require('../../../../utils/webhook_utils');
describe('Interactive Dialog - Apps Form', () => {
const inputTypes = {
realname: 'text',
someemail: 'email',
somenumber: 'number',
somepassword: 'password',
};
const optionsLength = {
someuserselector: 25, // default number of users in autocomplete
somechannelselector: 2, // town-square and off-topic for new team
someoptionselector: 3, // number of defined basic options
someradiooptions: 2, // number of defined basic options
};
let createdCommand;
let fullDialog;
before(() => {
cy.requireWebhookServer();
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for dialog',
display_name: 'Dialog',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'dialog',
url: `${webhookBaseUrl}/dialog_request`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
fullDialog = webhookUtils.getFullDialog(createdCommand.id, webhookBaseUrl);
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T2491 - UI check', () => {
// # Post a slash command
cy.get('#postListContent').should('be.visible');
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify that the header of modal contains icon URL, title and close button
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.get('#appsModalIconUrl').should('be.visible').and('have.attr', 'src').and('not.be.empty');
cy.get('#appsModalLabel').should('be.visible').and('have.text', fullDialog.dialog.title);
cy.wrap($elForm).find('button.close').should('be.visible').and('contain', '×').and('contain', 'Close');
cy.get('#appsModalLabel').should('be.visible').and('have.text', fullDialog.dialog.title);
});
// * Verify that the body contains all the elements
cy.get('.modal-body').should('be.visible').children('.form-group').each(($elForm, index) => {
const element = fullDialog.dialog.elements[index];
// Skip if element is undefined (more DOM elements than expected)
if (!element) {
return;
}
cy.wrap($elForm).within(() => {
cy.get('label').first().scrollIntoView().should('be.visible').and('contain', element.display_name);
if (['someuserselector', 'somechannelselector', 'someoptionselector'].includes(element.name)) {
// ReactSelect structure - check for MultiInput element
cy.get('[id^=\'MultiInput_\']').should('be.visible');
// * Verify that the dropdown opens on click
cy.get('[id^=\'MultiInput_\']').click();
// Break out of .within() scope to find options in document
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__menu').should('be.visible');
});
// # Click label to close any opened drop-downs
cy.get('label').first().click({force: true});
} else if (element.name === 'someradiooptions') {
cy.get('input').should('be.visible').and('have.length', optionsLength[element.name]);
// * Verify that no option is selected by default
cy.get('input').each(($elInput) => {
cy.wrap($elInput).should('not.be.checked');
});
} else if (element.name === 'boolean_input') {
cy.get('.checkbox').should('be.visible').within(() => {
cy.get('input[type=\'checkbox\']').
should('be.visible').
and('be.checked');
cy.get('span').should('have.text', element.placeholder);
});
} else {
cy.get(`#${element.name}`).should('be.visible').and('have.value', element.default || '').and('have.attr', 'placeholder', element.placeholder || '');
}
// * Verify that input element are given with the correct type of "input", "email", "number" and "password".
// * To take advantage of supported built-in validation.
if (inputTypes[element.name]) {
cy.get(`#${element.name}`).should('have.attr', 'type', inputTypes[element.name]);
}
if (element.help_text) {
cy.get('.help-text').should('exist').and('contain', element.help_text);
}
});
});
// * Verify that the footer contains cancel and submit buttons
cy.get('.modal-footer').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('#appsModalCancel').should('be.visible').and('have.text', 'Cancel');
cy.wrap($elForm).find('#appsModalSubmit').should('be.visible').and('have.text', fullDialog.dialog.submit_label);
});
closeAppsFormModal();
});
});
it('MM-T2492 - Cancel button works', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible');
// # Click cancel from the modal
cy.get('#appsModalCancel').click();
// * Verify that the apps form modal is closed
cy.get('#appsModal').should('not.exist');
});
it('MM-T2493 - "X" closes the dialog', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible');
// # Click "X" button from the modal
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
// * Verify that the apps form modal is closed
cy.get('#appsModal').should('not.exist');
});
it('MM-T2494 - Correct error messages displayed if empty form is submitted', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible');
// # Click submit button from the modal
cy.get('#appsModalSubmit').click();
// * Verify that the apps form modal is still open
cy.get('#appsModal').should('be.visible');
// * Verify that not optional element without text value shows an error and vice versa
cy.get('.modal-body').should('be.visible').children('.form-group').each(($elForm, index) => {
const element = fullDialog.dialog.elements[index];
if (!element.optional && !element.default) {
cy.wrap($elForm).find('div.error-text').should('exist').and('contain', 'This field is required.');
} else {
cy.wrap($elForm).find('div.error-text').should('not.exist');
}
});
closeAppsFormModal();
});
it('MM-T2495_1 - Email validation for invalid input', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the interactive dialog modal open up
cy.get('#appsModal').should('be.visible');
// # Enter invalid and valid email
// * Verify that error is: shown for invalid email and not shown for valid email.
const invalidEmail = 'invalid-email';
cy.get('#someemail').scrollIntoView().clear().type(invalidEmail);
cy.get('#appsModalSubmit').click();
cy.get('input:invalid').should('have.length', 1);
cy.get('#someemail').then(($input) => {
expect($input[0].validationMessage).to.eq(`Please include an '@' in the email address. '${invalidEmail}' is missing an '@'.`);
});
closeAppsFormModal();
});
it('MM-T2495_2 - Email validation for valid input', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the interactive dialog modal open up
cy.get('#appsModal').should('be.visible');
// # Enter valid email
// * Verify that error is not shown for valid email.
const validEmail = 'test@mattermost.com';
cy.get('#someemail').scrollIntoView().clear().type(validEmail);
cy.get('#appsModalSubmit').click();
cy.get('input:invalid').should('have.length', 0);
closeAppsFormModal();
});
it('MM-T2496_1 - Number validation for invalid input', () => {
cy.postMessage(`/${createdCommand.trigger} `);
cy.get('#appsModal').should('be.visible');
// # Enter invalid number
// * Verify that error is shown for invalid number.
const invalidNumber = 'invalid-number';
cy.get('#somenumber').scrollIntoView().clear().type(invalidNumber);
cy.get('#appsModalSubmit').click();
cy.get('.modal-body').should('be.visible').children('.form-group').eq(2).within(($elForm) => {
cy.wrap($elForm).find('div.error-text').should('exist').and('contain', 'This field is required.');
});
closeAppsFormModal();
});
it('MM-T2496_2 - Number validation for valid input', () => {
cy.postMessage(`/${createdCommand.trigger} `);
cy.get('#appsModal').should('be.visible');
// # Enter a valid number
// * Verify that error is not shown for valid number.
const validNumber = 12;
cy.get('#somenumber').scrollIntoView().clear().type(validNumber);
cy.get('#appsModalSubmit').click();
cy.get('.modal-body').should('be.visible').children('.form-group').eq(2).within(($elForm) => {
cy.wrap($elForm).find('div.error-text').should('not.exist');
});
closeAppsFormModal();
});
it('MM-T2501 - Password element check', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible');
// * Verify that the password text area is visible
cy.get('#somepassword').should('be.visible');
// * Verify that the password is masked on enter of text
cy.get('#somepassword').should('have.attr', 'type', 'password');
closeAppsFormModal();
});
});
function closeAppsFormModal() {
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
}

View File

@@ -0,0 +1,258 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @not_cloud @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
import * as TIMEOUTS from '@/fixtures/timeouts';
let createdCommand;
describe('Interactive Dialog - Multiform (Step-by-step Form Submissions)', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.requireWebhookServer();
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for multiform functionality - step by step form submissions',
display_name: 'Multiform Dialog Test',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'multiform_dialog',
url: `${webhookBaseUrl}/dialog/multistep`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T2550A - Multiform initial step (Step 1) UI verification', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up with Step 1
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify Step 1 dialog structure
cy.get('#appsModalLabel').should('contain', 'Step 1 - Personal Info');
cy.get('#appsModalSubmit').should('contain', 'Next Step');
// * Verify Step 1 fields are present
cy.get('.modal-body').within(() => {
cy.contains('First Name').should('be.visible');
cy.contains('Email').should('be.visible');
cy.get('.form-group').should('have.length', 2);
});
closeAppsFormModal();
});
});
it('MM-T2550B - Complete multiform workflow: Step 1 → Step 2 → Step 3', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify Step 1 opens
cy.get('#appsModal').should('be.visible').within(() => {
cy.get('#appsModalLabel').should('contain', 'Step 1 - Personal Info');
// # Fill out Step 1 form
cy.get('input[placeholder*="first name"]').type('John');
cy.get('input[placeholder*="email"]').type('john.doe@example.com');
// # Submit Step 1 - this should create a new dialog (Step 2)
cy.get('#appsModalSubmit').click();
});
// * Wait for Step 2 dialog to load (new form created via multiform)
cy.wait(TIMEOUTS.ONE_SEC);
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify new dialog has Step 2 title (multiform created new form)
cy.get('#appsModalLabel').should('contain', 'Step 2 - Work Info');
cy.get('#appsModalSubmit').should('contain', 'Next Step');
// * Verify Step 2 specific fields are present (different from Step 1)
cy.get('.modal-body').within(() => {
cy.contains('Department').should('be.visible');
cy.contains('Experience Level').should('be.visible');
// * Step 1 fields should not be present (this is a new dialog)
cy.contains('First Name').should('not.exist');
cy.contains('Email').should('not.exist');
});
// # Fill out Step 2 form
cy.get('.form-group').contains('Department').parent().within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.wait(TIMEOUTS.HALF_SEC);
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Engineering').click();
});
// Select experience level (radio)
cy.get('input[type="radio"][value="senior"]').click();
// # Submit Step 2 - this should create Step 3 dialog
cy.get('#appsModalSubmit').click();
});
// * Wait for Step 3 dialog to load (final step via multiform)
cy.wait(TIMEOUTS.ONE_SEC);
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify new dialog has Step 3 title and final submit
cy.get('#appsModalLabel').should('contain', 'Step 3 - Final Details');
cy.get('#appsModalSubmit').should('contain', 'Complete Registration');
// * Verify Step 3 specific fields
cy.get('.modal-body').within(() => {
cy.contains('Comments').should('be.visible');
cy.contains('Terms & Conditions').should('be.visible');
});
// # Fill out Step 3 (final step)
cy.get('textarea[placeholder*="comments"]').type('Multiform test completed successfully');
cy.get('input[type="checkbox"]').check();
// # Submit final step
cy.get('#appsModalSubmit').click();
});
// * Verify multiform completed and dialog closed
cy.get('#appsModal').should('not.exist');
// * Verify completion message with accumulated data
cy.getLastPost().should('contain', 'Multistep completed successfully');
cy.getLastPost().should('contain', 'Final step values');
});
it('MM-T2550C - Multiform step progression validation', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify initial Step 1
cy.get('#appsModal').should('be.visible').within(() => {
cy.get('#appsModalLabel').should('contain', 'Step 1 - Personal Info');
// # Submit empty form to test validation
cy.get('#appsModalSubmit').click();
});
cy.wait(TIMEOUTS.HALF_SEC);
// * Verify validation errors appear and we stay on Step 1
cy.get('#appsModal').should('be.visible').within(() => {
cy.get('#appsModalLabel').should('contain', 'Step 1 - Personal Info'); // Still on step 1
// * Check for validation errors on required fields
cy.get('.form-group').contains('First Name').parent().within(() => {
cy.get('.error-text').should('be.visible');
});
cy.get('.form-group').contains('Email').parent().within(() => {
cy.get('.error-text').should('be.visible');
});
closeAppsFormModal();
});
});
it('MM-T2550D - Multiform cancellation at different steps', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Test cancellation from Step 1
cy.get('#appsModal').should('be.visible').within(() => {
cy.get('#appsModalLabel').should('contain', 'Step 1 - Personal Info');
cy.get('#appsModalCancel').click();
});
cy.get('#appsModal').should('not.exist');
cy.getLastPost().should('contain', 'Dialog cancelled');
// # Start multiform again and progress to Step 2
cy.postMessage(`/${createdCommand.trigger} `);
cy.get('#appsModal').should('be.visible').within(() => {
cy.get('input[placeholder*="first name"]').type('Jane');
cy.get('input[placeholder*="email"]').type('jane@test.com');
cy.get('#appsModalSubmit').click();
});
// * Wait for Step 2 and cancel from there
cy.wait(TIMEOUTS.ONE_SEC);
cy.get('#appsModal').should('be.visible').within(() => {
cy.get('#appsModalLabel').should('contain', 'Step 2 - Work Info');
cy.get('#appsModalCancel').click();
});
cy.get('#appsModal').should('not.exist');
cy.getLastPost().should('contain', 'Dialog cancelled');
});
it('MM-T2550E - Multiform maintains step-specific content', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify each step has distinct content that doesn't carry over
cy.get('#appsModal').should('be.visible').within(() => {
// * Step 1 specific elements
cy.get('#appsModalLabel').should('contain', 'Step 1 - Personal Info');
cy.contains('First Name').should('be.visible');
cy.contains('Email').should('be.visible');
// # Fill and submit Step 1
cy.get('input[placeholder*="first name"]').type('Bob');
cy.get('input[placeholder*="email"]').type('bob@company.com');
cy.get('#appsModalSubmit').click();
});
cy.wait(TIMEOUTS.ONE_SEC);
cy.get('#appsModal').should('be.visible').within(() => {
// * Step 2 has completely different content
cy.get('#appsModalLabel').should('contain', 'Step 2 - Work Info');
cy.contains('Department').should('be.visible');
cy.contains('Experience Level').should('be.visible');
// * Step 1 content is not visible (new form)
cy.contains('First Name').should('not.exist');
cy.contains('Email').should('not.exist');
closeAppsFormModal();
});
});
});
function closeAppsFormModal() {
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
}

View File

@@ -0,0 +1,470 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @not_cloud @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
import * as TIMEOUTS from '@/fixtures/timeouts';
const webhookUtils = require('../../../../utils/webhook_utils');
let createdCommandWithDefaults;
let createdCommandClean;
let multiSelectDialogWithDefaults;
let multiSelectDialogClean;
describe('Interactive Dialog - Multiselect', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.requireWebhookServer();
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
// # Create new team and create commands on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
// Create command for tests with defaults
const commandWithDefaults = {
auto_complete: false,
description: 'Test for multiselect dialog elements with defaults',
display_name: 'Multiselect Dialog Test (Defaults)',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'multiselect_dialog_defaults',
url: `${webhookBaseUrl}/multiselect_dialog_request?includeDefaults=true`,
username: '',
};
// Create command for clean tests (no defaults)
const commandClean = {
auto_complete: false,
description: 'Test for multiselect dialog elements (clean)',
display_name: 'Multiselect Dialog Test (Clean)',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'multiselect_dialog_clean',
url: `${webhookBaseUrl}/multiselect_dialog_request?includeDefaults=false`,
username: '',
};
cy.apiCreateCommand(commandWithDefaults).then(({data}) => {
createdCommandWithDefaults = data;
multiSelectDialogWithDefaults = webhookUtils.getMultiSelectDialog(createdCommandWithDefaults.id, webhookBaseUrl, true);
});
cy.apiCreateCommand(commandClean).then(({data}) => {
createdCommandClean = data;
multiSelectDialogClean = webhookUtils.getMultiSelectDialog(createdCommandClean.id, webhookBaseUrl, false);
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T2510A - Multiselect default values verification', () => {
// # Post a slash command (with defaults)
cy.postMessage(`/${createdCommandWithDefaults.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify that the header contains correct title
cy.get('.modal-header').should('be.visible').within(() => {
cy.get('#appsModalLabel').should('be.visible').and('have.text', multiSelectDialogWithDefaults.dialog.title);
});
// * Verify default values are preselected correctly
cy.get('.modal-body').should('be.visible').children('.form-group').each(($elForm, index) => {
const element = multiSelectDialogWithDefaults.dialog.elements[index];
if (!element) {
return;
}
cy.wrap($elForm).within(() => {
if (element.name === 'multiselect_options') {
// * Verify multiselect options have default values (opt1,opt3 = Engineering, Marketing)
cy.get('.react-select__multi-value').should('have.length', 2);
cy.get('.react-select__multi-value').eq(0).should('contain', 'Engineering');
cy.get('.react-select__multi-value').eq(1).should('contain', 'Marketing');
} else if (element.name === 'multiselect_users') {
// * Verify multiselect users field has no default values
cy.get('.react-select__multi-value').should('not.exist');
} else if (element.name === 'single_select_options') {
// * Verify single select has default value (single2 = Single Option 2)
cy.get('.react-select__single-value').should('contain', 'Single Option 2');
}
});
});
closeAppsFormModal();
});
});
it('MM-T2510B - Multiselect UI and functionality (clean)', () => {
// # Post a slash command (clean, no defaults)
cy.postMessage(`/${createdCommandClean.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify that the header of modal contains icon URL, title and close button
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.get('#appsModalIconUrl').should('be.visible').and('have.attr', 'src').and('not.be.empty');
cy.get('#appsModalLabel').should('be.visible').and('have.text', multiSelectDialogClean.dialog.title);
cy.wrap($elForm).find('button.close').should('be.visible').and('contain', '×').and('contain', 'Close');
});
// * Verify that the body contains all multiselect elements
cy.get('.modal-body').should('be.visible').children('.form-group').should('have.length', 3).each(($elForm, index) => {
const element = multiSelectDialogClean.dialog.elements[index];
// Skip if element is undefined (more DOM elements than expected)
if (!element) {
return;
}
cy.wrap($elForm).within(() => {
// Verify the label text includes display name
cy.get('label').first().scrollIntoView().should('be.visible').and('contain', element.display_name);
if (element.name === 'multiselect_options') {
// * Verify multiselect options field starts empty
cy.get('[id^=\'MultiInput_\']').should('be.visible');
cy.get('.react-select__multi-value').should('not.exist');
// * Test adding multiple options
cy.get('[id^=\'MultiInput_\']').click();
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Engineering').click();
});
cy.get('[id^=\'MultiInput_\']').click();
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Sales').click();
});
// * Verify two options are now selected
cy.get('.react-select__multi-value').should('have.length', 2);
cy.get('.react-select__multi-value').eq(0).should('contain', 'Engineering');
cy.get('.react-select__multi-value').eq(1).should('contain', 'Sales');
// * Test removing an option
cy.get('.react-select__multi-value').eq(0).find('.react-select__multi-value__remove').click();
// * Verify only one option remains
cy.get('.react-select__multi-value').should('have.length', 1);
cy.get('.react-select__multi-value').eq(0).should('contain', 'Sales');
} else if (element.name === 'multiselect_users') {
// * Verify multiselect users field starts empty
cy.get('[id^=\'MultiInput_\']').should('be.visible');
cy.get('.react-select__multi-value').should('not.exist');
// * Test selecting multiple users
cy.get('[id^=\'MultiInput_\']').click();
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').should('have.length.at.least', 1);
cy.wrap(doc).find('.react-select__option').first().click();
});
cy.get('[id^=\'MultiInput_\']').click();
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').eq(1).click();
});
// * Verify two users are selected
cy.get('.react-select__multi-value').should('have.length', 2);
} else if (element.name === 'single_select_options') {
// * Verify single select field starts empty
cy.get('[id^=\'MultiInput_\']').should('be.visible');
// * Test selecting a single option
cy.get('[id^=\'MultiInput_\']').click();
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Single Option 3').click();
});
// * Verify single value was selected
cy.get('.react-select__single-value').should('contain', 'Single Option 3');
}
// * Verify help text if present
if (element.help_text) {
cy.get('.help-text').should('be.visible').and('contain', element.help_text);
}
});
});
// * Verify that the footer contains cancel and submit buttons
cy.get('.modal-footer').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('#appsModalCancel').should('be.visible').and('have.text', 'Cancel');
cy.wrap($elForm).find('#appsModalSubmit').should('be.visible').and('have.text', multiSelectDialogClean.dialog.submit_label);
});
closeAppsFormModal();
});
});
it('MM-T2511A - Multiselect form submission with defaults', () => {
// # Post a slash command (with defaults)
cy.postMessage(`/${createdCommandWithDefaults.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Select additional option in the multiselect field (defaults already present)
cy.get('.form-group').eq(0).within(() => {
// Keep default selections and add one more
cy.get('[id^=\'MultiInput_\']').click();
});
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Support').click();
});
// # Select multiple users
cy.get('.form-group').eq(1).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').first().click();
});
cy.get('.form-group').eq(1).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').eq(1).click();
});
// # Submit the form
cy.intercept('/api/v4/actions/dialogs/submit').as('submitAction');
cy.get('#appsModalSubmit').click();
});
// * Verify that the apps form modal is closed
cy.get('#appsModal').should('not.exist');
// * Verify that submitted values contain array values for multiselect
cy.wait('@submitAction').should('include.all.keys', ['request', 'response']).then((result) => {
const {submission} = result.request.body;
// * Verify multiselect options submitted as array with defaults + added option
expect(submission.multiselect_options).to.be.an('array');
expect(submission.multiselect_options).to.include.members(['opt1', 'opt3', 'opt4']); // Engineering, Marketing, Support
// * Verify multiselect users submitted as array
expect(submission.multiselect_users).to.be.an('array');
expect(submission.multiselect_users).to.have.length(2);
// * Verify single select submitted as string with default value
expect(submission.single_select_options).to.be.a('string');
expect(submission.single_select_options).to.equal('single2'); // Default value
});
// * Verify success message
cy.getLastPost().should('contain', 'Dialog submitted');
});
it('MM-T2511B - Multiselect form submission (clean)', () => {
// # Post a slash command (clean, no defaults)
cy.postMessage(`/${createdCommandClean.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Select multiple options in the multiselect field (starting from empty)
cy.get('.form-group').eq(0).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Engineering').click();
});
cy.get('.form-group').eq(0).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Sales').click();
});
// # Select multiple users
cy.get('.form-group').eq(1).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').first().click();
});
cy.get('.form-group').eq(1).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').eq(1).click();
});
// # Select single option
cy.get('.form-group').eq(2).within(() => {
cy.get('[id^=\'MultiInput_\']').click();
});
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').contains('Single Option 1').click();
});
// # Submit the form
cy.intercept('/api/v4/actions/dialogs/submit').as('submitAction');
cy.get('#appsModalSubmit').click();
});
// * Verify that the apps form modal is closed
cy.get('#appsModal').should('not.exist');
// * Verify that submitted values contain array values for multiselect
cy.wait('@submitAction').should('include.all.keys', ['request', 'response']).then((result) => {
const {submission} = result.request.body;
// * Verify multiselect options submitted as array
expect(submission.multiselect_options).to.be.an('array');
expect(submission.multiselect_options).to.include.members(['opt1', 'opt2']); // Engineering, Sales
// * Verify multiselect users submitted as array
expect(submission.multiselect_users).to.be.an('array');
expect(submission.multiselect_users).to.have.length(2);
// * Verify single select submitted as string
expect(submission.single_select_options).to.be.a('string');
expect(submission.single_select_options).to.equal('single1'); // Selected value
});
// * Verify success message
cy.getLastPost().should('contain', 'Dialog submitted');
});
it('MM-T2512 - Multiselect validation error handling', () => {
// # Post a slash command (clean, no defaults)
cy.postMessage(`/${createdCommandClean.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Required multiselect field (multiselect_options) starts empty in clean dialog
cy.get('.form-group').eq(0).within(() => {
// Verify the field shows required indicator (*) or field name
cy.get('label').should('contain', 'Multi Option Selector');
// Verify field starts empty (no clearing needed in clean dialog)
cy.get('.react-select__multi-value').should('not.exist');
});
cy.wait(TIMEOUTS.HALF_SEC); // Wait for potential validation to appear
// # Try to submit the form with empty required multiselect
cy.get('#appsModalSubmit').click();
});
// * Verify that the apps form modal is still open (validation failed)
cy.get('#appsModal').should('be.visible');
// * Verify error message appears for required field
cy.get('#appsModal').within(() => {
cy.get('.form-group').eq(0).within(() => {
cy.get('.error-text').should('be.visible').and('contain', 'This field is required');
});
});
closeAppsFormModal();
});
it('MM-T2513 - Multiselect with keyboard navigation', () => {
// # Post a slash command (clean, no defaults)
cy.postMessage(`/${createdCommandClean.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// # Test keyboard navigation in multiselect (starts clean, no defaults)
cy.get('.form-group').eq(0).within(() => {
// First selection: Use keyboard to select Sales option
cy.get('[id^=\'MultiInput_\']').click().type('{downarrow}{downarrow}{enter}'); // Select Sales option
// * Verify Sales was added
cy.get('.react-select__multi-value').should('have.length', 1);
cy.get('.react-select__multi-value').should('contain', 'Sales');
// # Test typing to filter options (dropdown closes after selection, so click again)
cy.get('[id^=\'MultiInput_\']').click().type('Prod{enter}'); // Type "Prod" to filter and select Product
// * Verify Product was added
cy.get('.react-select__multi-value').should('have.length', 2);
cy.get('.react-select__multi-value').should('contain', 'Product');
cy.wait(TIMEOUTS.HALF_SEC); // Wait for potential dropdown rendering
// # Test removing option (click remove button)
cy.get('.react-select__multi-value').contains('Product').parent().within(() => {
cy.get('.react-select__multi-value__remove').click();
});
// * Verify Product was removed
cy.get('.react-select__multi-value').should('have.length', 1);
cy.get('.react-select__multi-value').should('not.contain', 'Product');
});
});
// * Verify dropdown is closed (no options visible)
cy.get('body').then(($body) => {
if ($body.find('.react-select__menu').length > 0) {
cy.get('.react-select__menu').should('not.be.visible');
}
});
closeAppsFormModal();
});
it('MM-T2514 - Multiselect accessibility check', () => {
// # Post a slash command (clean, no defaults)
cy.postMessage(`/${createdCommandClean.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify multiselect has basic accessibility elements (uses clean dialog)
cy.get('.form-group').eq(0).within(() => {
// * Verify label exists and is visible
cy.get('label').should('be.visible').and('contain', 'Multi Option Selector');
// * Verify multiselect input is present and accessible
cy.get('[id^=\'MultiInput_\']').should('be.visible');
// * Verify help text exists and is visible
cy.get('.help-text').should('be.visible').and('contain', 'You can select multiple options');
// * Test basic interaction accessibility - click to open/close
cy.get('[id^=\'MultiInput_\']').click();
// * Verify dropdown opens (options become available)
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').should('have.length.at.least', 1);
});
});
// * Close dropdown by clicking elsewhere in the modal (force since dropdown options may cover the area)
cy.get('.modal-body').click({force: true});
});
closeAppsFormModal();
});
});
function closeAppsFormModal() {
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
}

View File

@@ -0,0 +1,164 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
const webhookUtils = require('../../../../utils/webhook_utils');
let createdCommand;
let userAndChannelDialog;
describe('Interactive Dialog - Apps Form', () => {
before(() => {
cy.requireWebhookServer();
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
for (let i = 0; i < 20; i++) {
cy.apiCreateChannel(team.id, `channel-${i}`, `Channel ${i}`);
}
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for user and channel dialog',
display_name: 'Dialog with user and channel',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'user_and_channel_dialog',
url: `${webhookBaseUrl}/user_and_channel_dialog_request`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
userAndChannelDialog = webhookUtils.getUserAndChannelDialog(createdCommand.id, webhookBaseUrl);
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T2498 - Individual "User" and "Channel" screens are scrollable', () => {
// # Ensure no modal is open before starting
cy.get('#appsModal').should('not.exist');
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify that the header of modal contains icon URL, title and close button
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.get('#appsModalIconUrl').should('be.visible').and('have.attr', 'src').and('not.be.empty');
cy.get('#appsModalLabel').should('be.visible').and('have.text', userAndChannelDialog.dialog.title);
cy.wrap($elForm).find('button.close').should('be.visible').and('contain', '×').and('contain', 'Close');
});
// * Verify that the body contains the both elements
cy.get('.modal-body').should('be.visible').children('.form-group').should('have.length', 2).each(($elForm, index) => {
const element = userAndChannelDialog.dialog.elements[index];
cy.wrap($elForm).find('label').first().scrollIntoView().should('be.visible').and('contain', element.display_name);
// ReactSelect structure - check for MultiInput element
cy.wrap($elForm).find('[id^=\'MultiInput_\']').should('be.visible');
// * Verify that the dropdown opens on click
cy.wrap($elForm).find('[id^=\'MultiInput_\']').click();
// Break out of .within() scope to find options in document and wait for them to be visible
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__menu').should('be.visible');
cy.wrap(doc).find('.react-select__option').should('have.length.greaterThan', 0);
});
if (index === 0) {
expect(element.name).to.equal('someuserselector');
// Test scrollability by navigating through ReactSelect options
cy.document().then((doc) => {
// Wait for options to be loaded and ensure first option exists
cy.wrap(doc).find('.react-select__option').first().should('exist');
// Navigate using keyboard on the input element
cy.wrap($elForm).find('[id^=\'MultiInput_\']').find('input').type('{uparrow}', {force: true});
cy.wrap($elForm).find('[id^=\'MultiInput_\']').find('input').type('{downarrow}'.repeat(10), {force: true});
// Verify scrolling happened by checking if first option is still in view
cy.wrap(doc).find('.react-select__option').first().then(($firstOption) => {
// Just verify the option exists - visibility may change due to scrolling
expect($firstOption.length).to.be.greaterThan(0);
});
cy.wrap($elForm).find('[id^=\'MultiInput_\']').find('input').type('{uparrow}'.repeat(10), {force: true});
cy.wrap(doc).find('.react-select__option').first().should('exist');
});
} else if (index === 1) {
expect(element.name).to.equal('somechannelselector');
// Test scrollability by navigating through ReactSelect options
cy.document().then((doc) => {
// Wait for options to be loaded and ensure first option exists
cy.wrap(doc).find('.react-select__option').first().should('exist');
// Navigate using keyboard on the input element
cy.wrap($elForm).find('[id^=\'MultiInput_\']').find('input').type('{uparrow}', {force: true});
cy.wrap($elForm).find('[id^=\'MultiInput_\']').find('input').type('{downarrow}'.repeat(10), {force: true});
// Verify scrolling happened by checking if first option is still in view
cy.wrap(doc).find('.react-select__option').first().then(($firstOption) => {
// Just verify the option exists - visibility may change due to scrolling
expect($firstOption.length).to.be.greaterThan(0);
});
cy.wrap($elForm).find('[id^=\'MultiInput_\']').find('input').type('{uparrow}'.repeat(10), {force: true});
cy.wrap(doc).find('.react-select__option').first().should('exist');
});
}
// # Select one element to close the dropdown
cy.document().then((doc) => {
cy.wrap(doc).find('.react-select__option').first().click({force: true});
});
if (element.help_text) {
cy.wrap($elForm).find('.help-text').should('exist').and('contain', element.help_text);
}
});
// * Verify that the footer contains cancel and submit buttons
cy.get('.modal-footer').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('#appsModalCancel').should('be.visible').and('have.text', 'Cancel');
cy.wrap($elForm).find('#appsModalSubmit').should('be.visible').and('have.text', userAndChannelDialog.dialog.submit_label);
});
// # Close interactive dialog
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
});
});
});

View File

@@ -0,0 +1,148 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @not_cloud @interactive_dialog
/**
* Note: This test requires webhook server running. Initiate `npm run start:webhook` to start.
*/
import * as TIMEOUTS from '@/fixtures/timeouts';
const webhookUtils = require('../../../../utils/webhook_utils');
let createdCommand;
let simpleDialog;
describe('Interactive Dialog - Apps Form without element', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.requireWebhookServer();
// # Ensure that teammate name display setting is set to default 'username'
cy.apiSaveTeammateNameDisplayPreference('username');
// # Create new team and create command on it
cy.apiCreateTeam('test-team', 'Test Team').then(({team}) => {
cy.visit(`/${team.name}`);
const webhookBaseUrl = Cypress.expose().webhookBaseUrl;
const command = {
auto_complete: false,
description: 'Test for simple dialog - no element',
display_name: 'Simple Dialog without element',
icon_url: '',
method: 'P',
team_id: team.id,
trigger: 'simple_dialog',
url: `${webhookBaseUrl}/simple_dialog_request`,
username: '',
};
cy.apiCreateCommand(command).then(({data}) => {
createdCommand = data;
simpleDialog = webhookUtils.getSimpleDialog(createdCommand.id, webhookBaseUrl);
});
});
});
afterEach(() => {
// # Reload current page after each test to close any dialogs left open
cy.reload();
});
it('MM-T2500_1 UI check', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible').within(() => {
// * Verify that the header of modal contains icon URL, title and close button
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.get('#appsModalIconUrl').should('be.visible').and('have.attr', 'src').and('not.be.empty');
cy.get('#appsModalLabel').should('be.visible').and('have.text', simpleDialog.dialog.title);
cy.wrap($elForm).find('button.close').should('be.visible').and('contain', '×').and('contain', 'Close');
});
// * Verify that the body exists but has no form fields (AppsForm always renders modal-body)
cy.get('.modal-body').should('be.visible');
cy.get('.modal-body').find('.form-group').should('not.exist');
// * Verify that the footer contains cancel and submit buttons
cy.get('.modal-footer').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('#appsModalCancel').should('be.visible').and('have.text', 'Cancel');
cy.wrap($elForm).find('#appsModalSubmit').should('be.visible').and('have.text', simpleDialog.dialog.submit_label);
});
closeAppsFormModal();
});
});
it('MM-T2500_2 "X" closes the dialog', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible');
// # Click "X" button from the modal
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click().wait(TIMEOUTS.FIVE_SEC);
});
// * Verify that the apps form modal is closed
cy.get('#appsModal').should('not.exist');
// * Verify that the last post states that the dialog is cancelled
cy.getLastPost().should('contain', 'Dialog cancelled');
});
it('MM-T2500_3 Cancel button works', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible');
// # Click cancel from the modal
cy.get('#appsModalCancel').click().wait(TIMEOUTS.FIVE_SEC);
// * Verify that the apps form modal is closed
cy.get('#appsModal').should('not.exist');
// * Verify that the last post states that the dialog is cancelled
cy.getLastPost().should('contain', 'Dialog cancelled');
});
it('MM-T2500_4 Submit button works', () => {
// # Post a slash command
cy.postMessage(`/${createdCommand.trigger} `);
// * Verify that the apps form modal opens up
cy.get('#appsModal').should('be.visible');
// # Click submit from the modal
cy.get('#appsModalSubmit').click();
// * Verify that the apps form modal is closed
cy.get('#appsModal').should('not.exist');
// * Verify that the last post states that the dialog is submitted
cy.getLastPost().should('contain', 'Dialog submitted');
});
});
function closeAppsFormModal() {
cy.get('.modal-header').should('be.visible').within(($elForm) => {
cy.wrap($elForm).find('button.close').should('be.visible').click();
});
cy.get('#appsModal').should('not.exist');
}