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
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:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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)');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
Reference in New Issue
Block a user