mirror of
https://github.com/mattermost/mattermost.git
synced 2026-08-26 21:27:40 -05:00
MM-68259 Fixing text and emojis being clipped in channel banners (#36076)
* Fixing text and emojis being clipped in channel banners - Added E2E tests that confirm that emojis respect the correct size and aren't clipping through container * Applying PR feedback * Running prettier on playwright tests * Fixing E2E Tests * Added test case for descending characters * Fixing linter issues * Resolving issue with e2e test failing
This commit is contained in:
@@ -5,6 +5,17 @@ export const appsPluginId = 'com.mattermost.apps';
|
||||
export const callsPluginId = 'com.mattermost.calls';
|
||||
export const playbooksPluginId = 'playbooks';
|
||||
|
||||
// License SKU short names — mirrored from webapp/channels/src/utils/constants.tsx LicenseSkus
|
||||
export const LicenseSkus = {
|
||||
E10: 'E10',
|
||||
E20: 'E20',
|
||||
Starter: 'starter',
|
||||
Professional: 'professional',
|
||||
Enterprise: 'enterprise',
|
||||
EnterpriseAdvanced: 'advanced',
|
||||
Entry: 'entry',
|
||||
} as const;
|
||||
|
||||
// Remote users hour limit taken from webapp/channels/src/utils/constants.ts
|
||||
export const REMOTE_USERS_HOUR_LIMIT_END_OF_THE_DAY = 22;
|
||||
export const REMOTE_USERS_HOUR_LIMIT_BEGINNING_OF_THE_DAY = 6;
|
||||
|
||||
@@ -8,6 +8,7 @@ export {TestBrowser} from './browser_context';
|
||||
export {getBlobFromAsset, getFileFromAsset} from './file';
|
||||
export {decomposeKorean, koreanTestPhrase, typeHangulCharacterWithIme, typeHangulWithIme} from './ime';
|
||||
export {duration, getRandomId, wait, newTestPassword} from './util';
|
||||
export {LicenseSkus, appsPluginId, callsPluginId, playbooksPluginId} from './constant';
|
||||
|
||||
export {
|
||||
ChannelsPage,
|
||||
|
||||
@@ -153,6 +153,12 @@ export default class ChannelsCenterView {
|
||||
await expect(this.channelBanner).not.toBeVisible();
|
||||
}
|
||||
|
||||
async assertChannelBannerTextNotClipped() {
|
||||
const bannerText = this.channelBanner.getByTestId('channel_banner_text');
|
||||
await expect(bannerText).toBeVisible();
|
||||
await this.assertElementContainedInBanner(bannerText);
|
||||
}
|
||||
|
||||
async assertChannelBannerHasBoldText(text: string) {
|
||||
const boldText = await this.channelBanner.locator('strong');
|
||||
expect(boldText).toBeVisible();
|
||||
@@ -176,4 +182,86 @@ export default class ChannelsCenterView {
|
||||
const actualText = await strikethroughText.textContent();
|
||||
expect(actualText).toBe(text);
|
||||
}
|
||||
|
||||
async assertChannelBannerHasEmoticon() {
|
||||
const emoji = this.channelBanner.locator('.emoticon:not(.emoticon--unicode)').first();
|
||||
await expect(emoji).toBeVisible();
|
||||
|
||||
const backgroundImage = await emoji.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image');
|
||||
});
|
||||
|
||||
expect(backgroundImage).not.toBe('none');
|
||||
}
|
||||
|
||||
async assertChannelBannerImageEmojiSize(expectedSizePx: number) {
|
||||
const emoji = this.channelBanner.locator('.emoticon:not(.emoticon--unicode)').first();
|
||||
await expect(emoji).toBeVisible();
|
||||
|
||||
const {width, height} = await emoji.evaluate((el) => {
|
||||
const styles = window.getComputedStyle(el);
|
||||
return {
|
||||
width: styles.getPropertyValue('width'),
|
||||
height: styles.getPropertyValue('height'),
|
||||
};
|
||||
});
|
||||
|
||||
expect(width).toBe(`${expectedSizePx}px`);
|
||||
expect(height).toBe(`${expectedSizePx}px`);
|
||||
|
||||
await this.assertElementContainedInBanner(emoji);
|
||||
}
|
||||
|
||||
async assertChannelBannerUnicodeEmojiSize(expectedSizePx: number) {
|
||||
const emoji = this.channelBanner.locator('.emoticon--unicode').first();
|
||||
await expect(emoji).toBeVisible();
|
||||
|
||||
const fontSize = await emoji.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('font-size');
|
||||
});
|
||||
|
||||
expect(fontSize).toBe(`${expectedSizePx}px`);
|
||||
|
||||
await this.assertElementContainedInBanner(emoji);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the given element's bounding box lies fully within the channel
|
||||
* banner's content area (banner bounds minus computed padding).
|
||||
*
|
||||
* Uses getBoundingClientRect() coordinates, which are NOT clipped by parent
|
||||
* overflow — so if an element protrudes into or beyond the padding zone it will
|
||||
* be visually clipped by `overflow: hidden` on the text container, and this
|
||||
* assertion will catch that.
|
||||
*
|
||||
* A small epsilon is applied to each boundary to avoid flaky failures caused
|
||||
* by sub-pixel rounding differences in layout engines.
|
||||
*/
|
||||
private async assertElementContainedInBanner(element: Locator) {
|
||||
const EPSILON = 0.5;
|
||||
|
||||
const bannerBox = await this.channelBanner.boundingBox();
|
||||
const elementBox = await element.boundingBox();
|
||||
|
||||
expect(bannerBox).not.toBeNull();
|
||||
expect(elementBox).not.toBeNull();
|
||||
|
||||
const banner = bannerBox!;
|
||||
const el = elementBox!;
|
||||
|
||||
const {paddingTop, paddingBottom, paddingLeft, paddingRight} = await this.channelBanner.evaluate((node) => {
|
||||
const styles = window.getComputedStyle(node);
|
||||
return {
|
||||
paddingTop: parseFloat(styles.paddingTop),
|
||||
paddingBottom: parseFloat(styles.paddingBottom),
|
||||
paddingLeft: parseFloat(styles.paddingLeft),
|
||||
paddingRight: parseFloat(styles.paddingRight),
|
||||
};
|
||||
});
|
||||
|
||||
expect(el.y).toBeGreaterThanOrEqual(banner.y + paddingTop - EPSILON);
|
||||
expect(el.y + el.height).toBeLessThanOrEqual(banner.y + banner.height - paddingBottom + EPSILON);
|
||||
expect(el.x).toBeGreaterThanOrEqual(banner.x + paddingLeft - EPSILON);
|
||||
expect(el.x + el.width).toBeLessThanOrEqual(banner.x + banner.width - paddingRight + EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
+83
-2
@@ -3,6 +3,8 @@
|
||||
|
||||
import {test} from '@mattermost/playwright-lib';
|
||||
|
||||
const EMOJI_SIZE = 16;
|
||||
|
||||
test('Should show channel banner when configured', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
@@ -89,9 +91,12 @@ test('Should not show channel banner in thread view when disabled', async ({pw})
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
await channelsPage.newChannel(pw.random.id(), 'O');
|
||||
await channelsPage.centerView.toBeVisible();
|
||||
|
||||
// Post a message and open the thread
|
||||
await channelsPage.centerView.postMessage('Message without banner');
|
||||
// Focus and type character-by-character to avoid React clearing a programmatic fill()
|
||||
await channelsPage.centerView.postCreate.input.click();
|
||||
await channelsPage.centerView.postCreate.input.pressSequentially('Message without banner');
|
||||
await channelsPage.centerView.postCreate.sendMessage();
|
||||
const post = await channelsPage.centerView.getLastPost();
|
||||
await post.reply();
|
||||
|
||||
@@ -99,6 +104,82 @@ test('Should not show channel banner in thread view when disabled', async ({pw})
|
||||
await channelsPage.sidebarRight.assertChannelBannerNotVisible();
|
||||
});
|
||||
|
||||
test('Should render image emoticons without clipping', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(license.SkuShortName !== 'advanced', 'Skipping test - server does not have Enterprise Advanced license');
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
await channelsPage.newChannel(pw.random.id(), 'O');
|
||||
|
||||
const channelSettingsModal = await channelsPage.openChannelSettings();
|
||||
const configurationTab = await channelSettingsModal.openConfigurationTab();
|
||||
|
||||
await configurationTab.enableChannelBanner();
|
||||
await configurationTab.setChannelBannerTextColor('77DD88');
|
||||
// :dog: is in Mattermost's emoji map → renders as .emoticon (background-image).
|
||||
// Unicode emojis that are also in the map (e.g. 🐶) follow the same path.
|
||||
await configurationTab.setChannelBannerText('Hello :dog:');
|
||||
await configurationTab.save();
|
||||
await channelSettingsModal.close();
|
||||
|
||||
await channelsPage.centerView.assertChannelBannerImageEmojiSize(EMOJI_SIZE);
|
||||
});
|
||||
|
||||
test('Should render unsupported unicode emoji without clipping', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(license.SkuShortName !== 'advanced', 'Skipping test - server does not have Enterprise Advanced license');
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
await channelsPage.newChannel(pw.random.id(), 'O');
|
||||
|
||||
const channelSettingsModal = await channelsPage.openChannelSettings();
|
||||
const configurationTab = await channelSettingsModal.openConfigurationTab();
|
||||
|
||||
await configurationTab.enableChannelBanner();
|
||||
await configurationTab.setChannelBannerTextColor('77DD88');
|
||||
// 🫠 (U+1FAE0, Unicode 14.0) is above Mattermost's emoji map ceiling (1FAD6)
|
||||
// so it falls through to the .emoticon--unicode span path.
|
||||
await configurationTab.setChannelBannerText('Hello 🫠');
|
||||
await configurationTab.save();
|
||||
await channelSettingsModal.close();
|
||||
|
||||
await channelsPage.centerView.assertChannelBannerUnicodeEmojiSize(EMOJI_SIZE);
|
||||
});
|
||||
|
||||
test('Should render text with descenders without clipping', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
test.skip(license.SkuShortName !== 'advanced', 'Skipping test - server does not have Enterprise Advanced license');
|
||||
|
||||
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
||||
await channelsPage.goto();
|
||||
await channelsPage.toBeVisible();
|
||||
|
||||
await channelsPage.newChannel(pw.random.id(), 'O');
|
||||
|
||||
const channelSettingsModal = await channelsPage.openChannelSettings();
|
||||
const configurationTab = await channelSettingsModal.openConfigurationTab();
|
||||
|
||||
await configurationTab.enableChannelBanner();
|
||||
await configurationTab.setChannelBannerTextColor('77DD88');
|
||||
// Characters with descenders (parts that extend below the baseline).
|
||||
// Previously clipped because line-height equalled font-size (13px), leaving
|
||||
// no room below the baseline for g, j, p, q, y etc.
|
||||
await configurationTab.setChannelBannerText('YyGgQqJj');
|
||||
await configurationTab.save();
|
||||
await channelSettingsModal.close();
|
||||
|
||||
await channelsPage.centerView.assertChannelBannerTextNotClipped();
|
||||
});
|
||||
|
||||
test('Should render markdown', async ({pw}) => {
|
||||
const {adminUser, adminClient} = await pw.initSetup();
|
||||
const license = await adminClient.getClientLicenseOld();
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
width: 100%;
|
||||
min-height: variables.$channel-banner-height;
|
||||
max-height: variables.$channel-banner-height;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-block: 5px;
|
||||
padding-block: 2px;
|
||||
padding-inline: 24px;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -15,7 +16,7 @@
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -24,10 +25,22 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.emoticon {
|
||||
width: 16px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
.emoticon--unicode {
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.markdown__heading {
|
||||
overflow: hidden;
|
||||
margin: 2px;
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user