Files
2bb8d01100 Mattermost Blocks (#36338)
* Mattermost Blocks

* Rethink some block props and styles

* Remove obsolete comment

* Style fixes and add examples

* Add mention, search, and image metadata support

* Several UI improvements

* Validate blocks by translating them

* Add initial e2e tests

* Fix lint

* Fix tests and lint

* Fix lint

* Improve e2e and simplify doPostAction

* server side cleanup

* Fix typo

* Fix style

* Fix some tests

* Fix more tests

* Fix cypress tests

* Fix playwhright tests

* Fix flaky system console test

* Frontend cleanup

* Add blocks editor

* Fix CI

* Limit all blocks width

* Fix test

* Enable webhook service on playwhright CI

* fix flaky test

* Fix style

* Cleanups

* Address coderabbit comments

* Address coderabbit comments

* Address CI errors

* Fix tests

* Address changes in valid actions

* UX fixes

* Fix test

* Fix danger buttons and add allow emojis in buttons

* style tweaks

use semantic color tokens for buttons and accents, update max-heights for scollables,  adjustments to containers, removed btn-sm (using default button size for mm blocks with height and padding adjustments for use within blocks. removed hover selector from utils to prevent hover state issue with primary button, refined semantic-color-warning.

* Address feedback

* Add interactive content to post previews

* Add collapse transitions

* Address autocomplete position

* Handle columns within column sets

* Fix css lint

* Fix tests

* Use feature flag

* Address feedback

* Fix lint and add missing tests

* Fix e2e tests

* Fix e2e test

* Allow interactive posts without a message

* Fix lint

* Address feedback

* Fix ci

* Fix lint

* Add limits to block translations

* Fix lint

* Address feedback

* Address feedback

* Update snapshot

* Fix collapsible not defaulting to the right value

* Add total text limit

* Sync translations with mobile

* Fix snapshot

---------

Co-authored-by: Matthew Birtch <[email protected]>
Co-authored-by: Mattermost Build <[email protected]>
2026-06-29 20:33:46 +02:00

326 lines
11 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import type {ChainableT} from '../../types';
function uiGetPostTextBox(option = {exist: true}): ChainableT<JQuery> {
if (option.exist) {
return cy.get('#post_textbox').should('be.visible');
}
return cy.get('#post_textbox').should('not.exist');
}
Cypress.Commands.add('uiGetPostTextBox', uiGetPostTextBox);
function uiGetReplyTextBox(option = {exist: true}): ChainableT<JQuery> {
if (option.exist) {
return cy.get('#reply_textbox').should('be.visible');
}
return cy.get('#reply_textbox').should('not.exist');
}
Cypress.Commands.add('uiGetReplyTextBox', uiGetReplyTextBox);
function uiGetPostProfileImage(postId: string): ChainableT<JQuery> {
return getPost(postId).within(() => {
return cy.get('.post__img').should('be.visible');
});
}
Cypress.Commands.add('uiGetPostProfileImage', uiGetPostProfileImage);
function uiGetPostHeader(postId: string): ChainableT<JQuery> {
return getPost(postId).within(() => {
return cy.get('.post__header').should('be.visible');
});
}
Cypress.Commands.add('uiGetPostHeader', uiGetPostHeader);
function uiGetPostBody(postId: string): ChainableT<JQuery> {
return getPost(postId).within(() => {
return cy.get('.post__body').should('be.visible');
});
}
Cypress.Commands.add('uiGetPostBody', uiGetPostBody);
function uiGetPostThreadFooter(postId: string): ChainableT<JQuery> {
return getPost(postId).find('.ThreadFooter');
}
Cypress.Commands.add('uiGetPostThreadFooter', uiGetPostThreadFooter);
function uiGetPostEmbedContainer(postId: string): ChainableT<JQuery> {
return cy.uiGetPostBody(postId).
find('.file-preview__button').
should('be.visible');
}
Cypress.Commands.add('uiGetPostEmbedContainer', uiGetPostEmbedContainer);
function getCenterPostById(postId: string): ChainableT<JQuery> {
// Prefer postView rows: virtualized lists may detach #post_<id> from a bare cy.get() query.
return cy.findAllByTestId('postView', {timeout: 20000}).
filter(`#post_${postId}`).
should('have.length.at.least', 1).
first().
scrollIntoView().
should('be.visible');
}
function getPost(postId: string): ChainableT<JQuery> {
if (postId) {
return getCenterPostById(postId);
}
return cy.getLastPost();
}
Cypress.Commands.add('getPost', getPost);
function getPostMmBlocks(postId: string): ChainableT<JQuery> {
return getCenterPostById(postId).find('.mm-blocks').should('exist');
}
Cypress.Commands.add('getPostMmBlocks', getPostMmBlocks);
function getRhsPostMmBlocks(postId: string): ChainableT<JQuery> {
return cy.findAllByTestId('rhsPostView', {timeout: 20000}).
filter(`#rhsPost_${postId}`).
should('have.length.at.least', 1).
first().
find('.mm-blocks').
should('exist');
}
Cypress.Commands.add('getRhsPostMmBlocks', getRhsPostMmBlocks);
function editLastPostWithNewMessage(message: string) {
cy.uiGetPostTextBox().type('{uparrow}');
// * Edit Post Input should appear
cy.get('#edit_textbox').should('be.visible');
// # Update the post message and click Save
cy.get('#edit_textbox').clear().type(message);
cy.get('#create_post').findByText('Save').scrollIntoView().click();
}
Cypress.Commands.add('editLastPostWithNewMessage', editLastPostWithNewMessage);
export function verifySavedPost(postId: string, message: string) {
// * Check that the center save icon has been updated correctly
cy.get(`#post_${postId}`).trigger('mouseover', {force: true});
cy.get(`#CENTER_flagIcon_${postId}`).
should('have.class', 'post-menu__item').
and('have.attr', 'aria-label', 'remove from saved');
// # Open the post-dotmenu
cy.clickPostDotMenu(postId, 'CENTER');
// * Check that the dotmenu item is changed accordingly
cy.findAllByTestId(`post-menu-${postId}`).eq(0).should('be.visible');
cy.findByText('Remove from Saved').scrollIntoView().should('be.visible');
cy.get('body').type('{esc}');
cy.get('#postListContent').within(() => {
// * Check that the post is highlighted
cy.get(`#post_${postId}`).should('have.class', 'post--pinned-or-flagged');
// * Check that the post pre-header is visible
cy.get('div.post-pre-header').should('be.visible');
// * Check that the post pre-header has the saved icon
cy.get('span.icon--post-pre-header').
should('be.visible').
within(() => {
cy.get('svg').should('have.attr', 'aria-label', 'Saved Icon');
});
// * Check that the post pre-header has the saved post link
cy.get('div.post-pre-header__text-container').
should('be.visible').
and('have.text', 'Saved').
within(() => {
cy.get('a').as('savedLink').should('be.visible');
});
});
// * Check that the saved posts list is not open in RHS before clicking the link in the post pre-header
cy.get('#searchContainer').should('not.exist');
// # Click the link
cy.get('@savedLink').click();
// * Check that the saved posts list is open in RHS
cy.get('#searchContainer').should('be.visible').within(() => {
cy.get('.sidebar--right__title').
should('be.visible').
and('have.text', 'Saved messages');
// * Check that the post pre-header is not shown for the saved message in RHS
cy.get(`#searchResult_${postId}`).within(() => {
cy.get(`#rhsPostMessageText_${postId}`).contains(message);
cy.get('div.post-pre-header').should('not.exist');
});
});
// # Close the RHS
cy.get('#searchResultsCloseButton').should('be.visible').click();
}
export function verifyUnsavedPost(postId: string) {
// * Check that the center save icon has been updated correctly
cy.get(`#post_${postId}`).trigger('mouseover', {force: true});
cy.get(`#CENTER_flagIcon_${postId}`).
should('have.class', 'post-menu__item').
and('have.attr', 'aria-label', 'save message');
// # Open the post-dotmenu
cy.clickPostDotMenu(postId, 'CENTER');
// * Check that the dotmenu item is changed accordingly
cy.findAllByTestId(`post-menu-${postId}`).eq(0).should('be.visible');
cy.findByText('Save Message').scrollIntoView().should('be.visible');
cy.get('body').type('{esc}');
cy.get('#postListContent').within(() => {
// * Check that the post is not highlighted
cy.get(`#post_${postId}`).should('not.have.class', 'post--pinned-or-flagged');
// * Check that the post pre-header is not visible
cy.get('div.post-pre-header').should('not.exist');
// * Check that the post pre-header does not have the saved icon
cy.get('span.icon--post-pre-header').
should('not.exist');
// * Check that the post pre-header does not have the saved post link
cy.get('div.post-pre-header__text-container').
should('not.exist');
});
// * Check that the saved posts list is not open in RHS before clicking the link in the post pre-header
cy.get('#searchContainer').should('not.exist');
// # Click the link
cy.uiGetSavedPostButton().click();
// * Check that the saved posts list is open in RHS
cy.get('#searchContainer').should('be.visible').within(() => {
cy.get('.sidebar--right__title').
should('be.visible').
and('have.text', 'Saved messages');
// * Check that the post pre-header is not shown for the saved message in RHS
cy.get('#search-items-container').within(() => {
cy.get(`#rhsPostMessageText_${postId}`).should('not.exist');
});
});
// # Close the RHS
cy.get('#searchResultsCloseButton').should('be.visible').click();
}
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Edit last post with a new message
*
* @param {string} - message
*
* @example
* cy.editLastPostWithNewMessage('new message');
*/
editLastPostWithNewMessage: typeof editLastPostWithNewMessage;
/**
* Get post profile image of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostProfileImage();
*/
uiGetPostProfileImage: typeof uiGetPostProfileImage;
/**
* Get post header of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostHeader();
*/
uiGetPostHeader: typeof uiGetPostHeader;
/**
* Get post body of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostBody();
*/
uiGetPostBody: typeof uiGetPostBody;
/**
* Get post thread footer of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostThreadFooter();
*/
uiGetPostThreadFooter: typeof uiGetPostThreadFooter;
/**
* Get post embed container of a given post ID or the last post if post ID is not given
*
* @param {string} - postId (optional)
*
* @example
* cy.uiGetPostEmbedContainer();
*/
uiGetPostEmbedContainer: typeof uiGetPostEmbedContainer;
/**
* Get post textbox
*
* @param {bool} option.exist - Set to false to check whether element should not exist. Otherwise, true (default) to check visibility.
*
* @example
* cy.uiGetPostTextBox();
*/
uiGetPostTextBox: typeof uiGetPostTextBox;
/**
* Get reply textbox
*
* @param {bool} option.exist - Set to false to check whether element should not exist. Otherwise, true (default) to check visibility.
*
* @example
* cy.uiGetReplyTextBox();
*/
uiGetReplyTextBox: typeof uiGetReplyTextBox;
getPost: typeof getPost;
/**
* Get mm_blocks root within a center-channel post.
*
* @param {string} postId
*
* @example
* cy.getPostMmBlocks(postId).within(() => { ... });
*/
getPostMmBlocks: typeof getPostMmBlocks;
/**
* Get mm_blocks root within an RHS/thread post row.
*
* @param {string} postId
*
* @example
* cy.getRhsPostMmBlocks(postId).within(() => { ... });
*/
getRhsPostMmBlocks: typeof getRhsPostMmBlocks;
}
}
}