mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* Fix classification modal save state Co-authored-by: mattermost-code <[email protected]> * Align classification modal regression test Co-authored-by: mattermost-code <[email protected]> * Reset save panel on classification edits Co-authored-by: mattermost-code <[email protected]> * Wait for classification sync before clearing edits Co-authored-by: mattermost-code <[email protected]> * Use realistic classified channel in reset test Co-authored-by: mattermost-code <[email protected]> * Strengthen classification banner regression test Co-authored-by: mattermost-code <[email protected]> * Refactored fix to be simpler, added E2E test suite to prevent regressions * Applied prettier fix to playwright test * Fixing failed E2E test --------- Co-authored-by: mattermost-code <[email protected]> Co-authored-by: avasconcelos114 <[email protected]> Co-authored-by: Mattermost Build <[email protected]> Co-authored-by: Hanzo Dev <[email protected]>
435 lines
20 KiB
TypeScript
435 lines
20 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
/**
|
|
* Channel Classification E2E tests.
|
|
* Tests the classification level assignment feature on both new and existing channels.
|
|
*
|
|
* Prerequisites: Enterprise-tier license + ClassificationMarkings feature flag enabled.
|
|
*/
|
|
|
|
import {expect, test, getAdminClient, licenseTier} from '@mattermost/playwright-lib';
|
|
import type {PlaywrightExtended} from '@mattermost/playwright-lib';
|
|
|
|
import {
|
|
TEST_LEVELS,
|
|
setClassificationMarkingsFeatureFlag,
|
|
setupClassificationWithChannelField,
|
|
deleteClassificationFieldsIfExist,
|
|
} from './helpers';
|
|
import type {ClassificationLevel} from './helpers';
|
|
|
|
let classificationLevels: ClassificationLevel[] = [];
|
|
let setupComplete = false;
|
|
|
|
// Teams created by pw.initSetup() in each test are tracked here and deleted in
|
|
// afterEach so local environments don't accumulate stale teams across runs.
|
|
const createdTeamIds: string[] = [];
|
|
|
|
async function initSetupTracked(pw: PlaywrightExtended) {
|
|
const setup = await pw.initSetup();
|
|
createdTeamIds.push(setup.team.id);
|
|
return setup;
|
|
}
|
|
|
|
test.beforeAll(async () => {
|
|
const {adminClient} = await getAdminClient();
|
|
const license = await adminClient.getClientLicenseOld();
|
|
if (licenseTier(license.SkuShortName) < 20) {
|
|
return;
|
|
}
|
|
|
|
await setClassificationMarkingsFeatureFlag(adminClient, true);
|
|
const setup = await setupClassificationWithChannelField(adminClient);
|
|
classificationLevels = setup.levels;
|
|
setupComplete = true;
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
if (!setupComplete) {
|
|
return;
|
|
}
|
|
const {adminClient} = await getAdminClient();
|
|
try {
|
|
await deleteClassificationFieldsIfExist(adminClient);
|
|
} catch {
|
|
// Best-effort cleanup
|
|
}
|
|
});
|
|
|
|
test.beforeEach(async () => {
|
|
const {adminClient} = await getAdminClient();
|
|
const license = await adminClient.getClientLicenseOld();
|
|
test.skip(licenseTier(license.SkuShortName) < 20, 'Channel classification requires Enterprise-tier license');
|
|
test.skip(!setupComplete, 'Classification levels were not set up');
|
|
|
|
const config = await adminClient.getConfig();
|
|
test.skip(
|
|
config.FeatureFlags.ClassificationMarkings !== true,
|
|
'ClassificationMarkings feature flag could not be enabled',
|
|
);
|
|
});
|
|
|
|
test.afterEach(async () => {
|
|
if (createdTeamIds.length === 0) {
|
|
return;
|
|
}
|
|
const ids = createdTeamIds.splice(0);
|
|
try {
|
|
const {adminClient} = await getAdminClient({skipLog: true});
|
|
await Promise.allSettled(ids.map((id) => adminClient.deleteTeam(id)));
|
|
} catch {
|
|
// Best-effort cleanup
|
|
}
|
|
});
|
|
|
|
test.describe('Channel Classification - New channel creation', () => {
|
|
test('Enabling classification toggle without selecting values prevents channel creation', async ({pw}) => {
|
|
const {adminUser, team} = await initSetupTracked(pw);
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
const newChannelModal = await channelsPage.openNewChannelModal();
|
|
await newChannelModal.fillDisplayName(`test-${pw.random.id()}`);
|
|
await newChannelModal.publicTypeButton.click();
|
|
|
|
// Create button should be enabled before toggling classification
|
|
await expect(newChannelModal.createButton).toBeEnabled();
|
|
|
|
// Enable classification toggle
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await classificationToggle.click();
|
|
|
|
// Create button should be disabled (no classification level selected, no banner text)
|
|
await expect(newChannelModal.createButton).toBeDisabled();
|
|
});
|
|
|
|
test('Classification dropdown displays the correct levels from the template', async ({pw}) => {
|
|
const {adminUser, team} = await initSetupTracked(pw);
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
const newChannelModal = await channelsPage.openNewChannelModal();
|
|
await newChannelModal.fillDisplayName(`test-${pw.random.id()}`);
|
|
|
|
// Enable classification toggle
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await classificationToggle.click();
|
|
|
|
// Open the classification dropdown
|
|
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
|
|
await dropdownContainer.click();
|
|
|
|
// Verify all test levels are present in the dropdown menu
|
|
const menu = channelsPage.page.locator('.DropDown__menu');
|
|
await expect(menu).toBeVisible();
|
|
for (const level of TEST_LEVELS) {
|
|
await expect(menu.getByText(level.name, {exact: true})).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('User can append text to the Banner Text field after selecting a classification', async ({pw}) => {
|
|
const {adminUser, team} = await initSetupTracked(pw);
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
const selectedLevel = classificationLevels.find((l) => l.name === 'SECRET');
|
|
expect(selectedLevel).toBeDefined();
|
|
|
|
const newChannelModal = await channelsPage.openNewChannelModal();
|
|
await newChannelModal.fillDisplayName(`test-${pw.random.id()}`);
|
|
|
|
// Enable classification toggle
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await classificationToggle.click();
|
|
|
|
// Select a classification level
|
|
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
|
|
await dropdownContainer.click();
|
|
const menu = channelsPage.page.locator('.DropDown__menu');
|
|
await menu.getByText(selectedLevel!.name, {exact: true}).click();
|
|
|
|
// Banner text should be auto-populated with the bold level name
|
|
const bannerTextbox = channelsPage.page.locator('#channel_classification_banner_text');
|
|
await expect(bannerTextbox).toBeVisible();
|
|
const currentValue = await bannerTextbox.inputValue();
|
|
expect(currentValue).toContain(selectedLevel!.name);
|
|
|
|
// Append custom text to the banner
|
|
await bannerTextbox.click();
|
|
await bannerTextbox.press('End');
|
|
await bannerTextbox.pressSequentially(' - Custom suffix');
|
|
|
|
const updatedValue = await bannerTextbox.inputValue();
|
|
expect(updatedValue).toContain('Custom suffix');
|
|
});
|
|
|
|
test('Creating channel with classification shows banner with correct color', async ({pw}) => {
|
|
const {adminUser, team} = await initSetupTracked(pw);
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
const selectedLevel = classificationLevels.find((l) => l.name === 'SECRET');
|
|
expect(selectedLevel).toBeDefined();
|
|
|
|
const newChannelModal = await channelsPage.openNewChannelModal();
|
|
await newChannelModal.fillDisplayName(`classified-${pw.random.id()}`);
|
|
await newChannelModal.publicTypeButton.click();
|
|
|
|
// Enable classification toggle
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await classificationToggle.click();
|
|
|
|
// Select the classification level
|
|
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
|
|
await dropdownContainer.click();
|
|
const menu = channelsPage.page.locator('.DropDown__menu');
|
|
await menu.getByText(selectedLevel!.name, {exact: true}).click();
|
|
|
|
// Wait for banner text to auto-populate, then create the channel
|
|
const bannerTextbox = channelsPage.page.locator('#channel_classification_banner_text');
|
|
await expect(bannerTextbox).toBeVisible();
|
|
await expect(bannerTextbox).not.toHaveValue('');
|
|
|
|
await newChannelModal.create();
|
|
|
|
// Should be redirected to the new channel and center view loads
|
|
await expect(channelsPage.page).toHaveURL(/\/channels\//, {timeout: 30000});
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 30000});
|
|
|
|
// Channel banner should be visible (allow extra time for property value fetch)
|
|
const banner = channelsPage.page.getByTestId('channel_banner_container');
|
|
await expect(banner).toBeVisible({timeout: 30000});
|
|
|
|
// Verify the banner has the correct background color
|
|
const actualBackgroundColor = await banner.evaluate((el) => {
|
|
return window.getComputedStyle(el).getPropertyValue('background-color');
|
|
});
|
|
const expectedRgb = hexToRgb(selectedLevel!.color);
|
|
expect(actualBackgroundColor).toBe(expectedRgb);
|
|
|
|
// Verify the banner contains the classification level name (rendered from **SECRET** markdown)
|
|
await expect(banner).toContainText(selectedLevel!.name);
|
|
});
|
|
});
|
|
|
|
test.describe('Channel Classification - Existing channel settings', () => {
|
|
test('Classification toggle can be enabled from channel settings', async ({pw}) => {
|
|
const {adminUser, team, adminClient} = await initSetupTracked(pw);
|
|
|
|
const channel = await adminClient.createChannel(
|
|
pw.random.channel({teamId: team.id, name: `cls-${pw.random.id()}`, displayName: `Cls ${pw.random.id()}`}),
|
|
);
|
|
await adminClient.addToChannel(adminUser.id, channel.id);
|
|
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name, channel.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
const channelSettingsModal = await channelsPage.openChannelSettings();
|
|
await channelSettingsModal.openConfigurationTab();
|
|
|
|
// The classification toggle should be available
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await expect(classificationToggle).toBeVisible();
|
|
|
|
// Toggle it on
|
|
const classes = await classificationToggle.getAttribute('class');
|
|
if (!classes?.includes('active')) {
|
|
await classificationToggle.click();
|
|
}
|
|
|
|
// Toggle should now be active
|
|
await expect(classificationToggle).toHaveClass(/active/);
|
|
});
|
|
|
|
test('Classification level can be set once toggle is enabled', async ({pw}) => {
|
|
const {adminUser, team, adminClient} = await initSetupTracked(pw);
|
|
|
|
const channel = await adminClient.createChannel(
|
|
pw.random.channel({teamId: team.id, name: `cls-${pw.random.id()}`, displayName: `Cls ${pw.random.id()}`}),
|
|
);
|
|
await adminClient.addToChannel(adminUser.id, channel.id);
|
|
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name, channel.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
const channelSettingsModal = await channelsPage.openChannelSettings();
|
|
await channelSettingsModal.openConfigurationTab();
|
|
|
|
// Enable classification toggle
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await classificationToggle.click();
|
|
|
|
// Classification level dropdown should be visible
|
|
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
|
|
await expect(dropdownContainer).toBeVisible();
|
|
|
|
// Open dropdown and select a level
|
|
await dropdownContainer.click();
|
|
const menu = channelsPage.page.locator('.DropDown__menu');
|
|
await expect(menu).toBeVisible();
|
|
|
|
const selectedLevel = classificationLevels.find((l) => l.name === 'CONFIDENTIAL');
|
|
expect(selectedLevel).toBeDefined();
|
|
await menu.getByText(selectedLevel!.name, {exact: true}).click();
|
|
|
|
// The dropdown should now show the selected value
|
|
await expect(dropdownContainer.getByText(selectedLevel!.name, {exact: true})).toBeVisible();
|
|
});
|
|
|
|
test('Selecting classification locks banner toggle active and disabled, with matching color', async ({pw}) => {
|
|
const {adminUser, team, adminClient} = await initSetupTracked(pw);
|
|
|
|
const channel = await adminClient.createChannel(
|
|
pw.random.channel({teamId: team.id, name: `cls-${pw.random.id()}`, displayName: `Cls ${pw.random.id()}`}),
|
|
);
|
|
await adminClient.addToChannel(adminUser.id, channel.id);
|
|
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name, channel.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
const channelSettingsModal = await channelsPage.openChannelSettings();
|
|
await channelSettingsModal.openConfigurationTab();
|
|
|
|
// Enable classification and select a level
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await classificationToggle.click();
|
|
|
|
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
|
|
await dropdownContainer.click();
|
|
|
|
const selectedLevel = classificationLevels.find((l) => l.name === 'SECRET');
|
|
expect(selectedLevel).toBeDefined();
|
|
const menu = channelsPage.page.locator('.DropDown__menu');
|
|
await menu.getByText(selectedLevel!.name, {exact: true}).click();
|
|
|
|
// The channel banner toggle should now be forced active and disabled
|
|
const bannerToggle = channelsPage.page.getByTestId('channelBannerToggle-button');
|
|
await expect(bannerToggle).toBeVisible();
|
|
await expect(bannerToggle).toHaveClass(/active/);
|
|
await expect(bannerToggle).toBeDisabled();
|
|
|
|
// Banner color input should be locked to the classification color
|
|
const colorInput = channelsPage.page.locator('#channel_banner_banner_background_color_picker-inputColorValue');
|
|
await expect(colorInput).toBeVisible();
|
|
const colorValue = await colorInput.inputValue();
|
|
expect(colorValue.toLowerCase().replace('#', '')).toBe(selectedLevel!.color.toLowerCase().replace('#', ''));
|
|
});
|
|
|
|
test('Disabling classification with a custom banner color preserves it server-side, and re-enabling restores the Save button', async ({
|
|
pw,
|
|
}) => {
|
|
const {adminUser, team, adminClient} = await initSetupTracked(pw);
|
|
|
|
const channel = await adminClient.createChannel(
|
|
pw.random.channel({teamId: team.id, name: `cls-${pw.random.id()}`, displayName: `Cls ${pw.random.id()}`}),
|
|
);
|
|
await adminClient.addToChannel(adminUser.id, channel.id);
|
|
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name, channel.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
// Step 1: enable classification, select a level, save
|
|
const channelSettingsModal = await channelsPage.openChannelSettings();
|
|
const configurationTab = await channelSettingsModal.openConfigurationTab();
|
|
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await classificationToggle.click();
|
|
|
|
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
|
|
await dropdownContainer.click();
|
|
const selectedLevel = classificationLevels.find((l) => l.name === 'SECRET')!;
|
|
await channelsPage.page.locator('.DropDown__menu').getByText(selectedLevel.name, {exact: true}).click();
|
|
await configurationTab.save();
|
|
|
|
// Step 2: in the same open modal, disable classification, enable a manual banner,
|
|
// set a custom color, save. The manual banner toggle is needed because master no
|
|
// longer auto-enables banner_info.enabled when classification is on, so the banner
|
|
// section body (and its color input) is hidden after toggling classification off
|
|
// until a manual banner is enabled.
|
|
await classificationToggle.click();
|
|
await configurationTab.enableChannelBanner();
|
|
const customColor = 'aa00aa';
|
|
await configurationTab.setChannelBannerBackgroundColor(customColor);
|
|
await configurationTab.save();
|
|
|
|
// Symptom 1 guard: color input reflects what we typed and the server persisted the custom color
|
|
const colorInput = channelsPage.page.locator('#channel_banner_banner_background_color_picker-inputColorValue');
|
|
await expect(colorInput).toHaveValue(`#${customColor}`);
|
|
const persisted = await adminClient.getChannel(channel.id);
|
|
expect(persisted.banner_info?.background_color?.toLowerCase().replace('#', '')).toBe(customColor);
|
|
|
|
// Symptom 2 guard: re-enable classification → Save button reappears (panel transitions out of 'saved')
|
|
await classificationToggle.click();
|
|
const saveButton = channelsPage.page.getByTestId('SaveChangesPanel__save-btn');
|
|
await expect(saveButton).toBeVisible();
|
|
await expect(saveButton).toBeEnabled();
|
|
|
|
await channelSettingsModal.close();
|
|
});
|
|
|
|
test('Editing banner text and saving updates the banner in real time', async ({pw}) => {
|
|
const {adminUser, team, adminClient} = await initSetupTracked(pw);
|
|
|
|
const channel = await adminClient.createChannel(
|
|
pw.random.channel({teamId: team.id, name: `cls-${pw.random.id()}`, displayName: `Cls ${pw.random.id()}`}),
|
|
);
|
|
await adminClient.addToChannel(adminUser.id, channel.id);
|
|
|
|
const {channelsPage} = await pw.testBrowser.login(adminUser);
|
|
await channelsPage.goto(team.name, channel.name);
|
|
await expect(channelsPage.page.getByTestId('channel_view')).toBeVisible({timeout: 60000});
|
|
|
|
const channelSettingsModal = await channelsPage.openChannelSettings();
|
|
const configurationTab = await channelSettingsModal.openConfigurationTab();
|
|
|
|
// Enable classification and select a level
|
|
const classificationToggle = channelsPage.page.getByTestId('channelClassificationToggle-button');
|
|
await classificationToggle.click();
|
|
|
|
const dropdownContainer = channelsPage.page.getByTestId('channelClassificationLevel');
|
|
await dropdownContainer.click();
|
|
|
|
const selectedLevel = classificationLevels.find((l) => l.name === 'TOP SECRET');
|
|
expect(selectedLevel).toBeDefined();
|
|
const menu = channelsPage.page.locator('.DropDown__menu');
|
|
await menu.getByText(selectedLevel!.name, {exact: true}).click();
|
|
|
|
// Edit the banner text to a custom value
|
|
const customBannerText = 'TOP SECRET - Handle via COMINT channels only';
|
|
const bannerTextbox = channelsPage.page.locator('#channel_banner_banner_text_textbox');
|
|
await expect(bannerTextbox).toBeVisible();
|
|
await bannerTextbox.fill(customBannerText);
|
|
|
|
// Save the changes
|
|
await configurationTab.save();
|
|
await channelSettingsModal.close();
|
|
|
|
// The channel banner should now show the custom text with the classification color
|
|
const banner = channelsPage.page.getByTestId('channel_banner_container');
|
|
await expect(banner).toBeVisible({timeout: 30000});
|
|
await expect(banner).toContainText(customBannerText);
|
|
|
|
const actualBackgroundColor = await banner.evaluate((el) => {
|
|
return window.getComputedStyle(el).getPropertyValue('background-color');
|
|
});
|
|
expect(actualBackgroundColor).toBe(hexToRgb(selectedLevel!.color));
|
|
});
|
|
});
|
|
|
|
function hexToRgb(hex: string): string {
|
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
if (!result) {
|
|
return hex;
|
|
}
|
|
return `rgb(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)})`;
|
|
}
|