Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions e2e-tests/playwright/lib/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
1 change: 1 addition & 0 deletions e2e-tests/playwright/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
88 changes: 88 additions & 0 deletions e2e-tests/playwright/lib/src/ui/components/channels/center_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -89,16 +91,95 @@ 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();

await channelsPage.sidebarRight.toBeVisible();
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();
Expand Down
19 changes: 16 additions & 3 deletions webapp/channels/src/components/channel_banner/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,7 +16,7 @@
overflow: hidden;
max-width: 100%;
font-size: 13px;
line-height: 13px;
line-height: 20px;
text-align: center;
text-overflow: ellipsis;

Expand All @@ -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;
}

Expand Down
Loading