Files
Harrison HealeyandGitHub daec1283e5 MM-69003 Mostly share ESLint config between web app and E2E tests (#36767)
* Switch Cypress to use shared ESLint config

* Run --fix in Cypress

* Manually fix remaining lint issues in Cypress

* Switch Playwright to use shared ESLint config

* Run --fix in Playwright

* Manually fix remaining lint issues in Playwright

* Install and cache web app deps during Cypress CI builds

This also caches the types and client package. That isn't needed currently
since it uses prepackaged versions of those, but I imagine we might change
that at some point.

* Run e2e-tests-check when ESLint plugin is updated

* Change E2E test GHA caching to cache all of web app node_modules

* Fix mismatch between cache save and restore

* Try bumping cache keys

* Copy step to install dependencies to server.run_cypress.sh

I don't know how this must've worked before, but if this fixes the issue,
it seems like neither Cypress nor Playwright actually use the cached
depenendencies.

* Try disabling caching entirely for Cypress tests

* Try bypassing makefile?

* Try also manually building dependencies in run_specs.sh

I don't know why this appears to duplicate run_cypress.sh and
run_playwright.sh, both of which are called run_test.sh which
might not be used any more as best I can tell.

* Try installing the web app dependencies in yet another place

* Disable the extra steps in server.prepare.sh specifically for Cypress

* Revert changes to update cache key and disable web app depenedency cache on Cypress builds
2026-06-09 20:50:58 +00:00

517 lines
23 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
ChannelsPost,
disableAutotranslationConfig,
enableAutotranslationConfig,
enableChannelAutotranslation,
getAdminClient,
hasAutotranslationLicense,
setUserChannelAutotranslation,
expect,
test,
setMockSourceLanguage,
} from '@mattermost/playwright-lib';
// Autotranslation tests involve real UI interactions with plugin state and can run
// longer than the default 60 s in loaded CI. Set per-test timeout to 2 minutes.
test.beforeEach(async () => {
test.setTimeout(120000);
});
// Disable AutoTranslationSettings at end of file so leftover state cannot leak
// into other suites. Individual tests enable the feature via
// enableAutotranslationConfig() as needed.
test.afterAll(async () => {
try {
const {adminClient} = await getAdminClient({skipLog: true});
await disableAutotranslationConfig(adminClient);
} catch {
// Best-effort cleanup.
}
});
test(
'translated message has indicator; click opens Show Translation modal',
{
tag: ['@autotranslation'],
},
async ({pw}) => {
const {adminClient, user, userClient, team} = await pw.initSetup();
const license = await adminClient.getClientLicenseOld();
test.skip(
!hasAutotranslationLicense(license.SkuShortName),
'Skipping test - server does not have Entry or Advanced license',
);
const translationUrl = process.env.TRANSLATION_SERVICE_URL || 'http://localhost:3010';
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
const channelName = `autotranslation-modal-${pw.random.id()}`;
const created = await adminClient.createChannel({
team_id: team.id,
name: channelName,
display_name: 'Show Translation Modal Test',
type: 'O',
});
await enableChannelAutotranslation(adminClient, created.id);
await adminClient.addToChannel(user.id, created.id);
await setUserChannelAutotranslation(userClient, created.id, true);
// Re-apply config immediately before posting so the server translates this message.
// A concurrent initSetup() can reset AutoTranslationSettings.Enable to false.
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
// Confirm Enable=true before posting — translation happens at creation time,
// so the config must be confirmed before posts are submitted.
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
// Verify the mock translation service is reachable before attempting to use it.
try {
await fetch(translationUrl, {signal: AbortSignal.timeout(3000)});
} catch {
test.skip(
true,
`Mock translation service not reachable at ${translationUrl}. ` +
'Start the service or set TRANSLATION_SERVICE_URL to run this test.',
);
return;
}
// Set Spanish source so the mock returns source='es', triggering es→en translation.
await setMockSourceLanguage(translationUrl, 'es');
const poster = await pw.random.user('poster');
const createdPoster = await adminClient.createUser(poster, '', '');
await adminClient.addToTeam(team.id, createdPoster.id);
await adminClient.addToChannel(createdPoster.id, created.id);
const {client: posterClient} = await pw.makeClient({
username: poster.username,
password: poster.password,
});
if (!posterClient) {
throw new Error('Failed to create poster client');
}
// Create a second poster to show translation indicator (only visible with multiple users)
const poster2 = await pw.random.user('poster2');
const createdPoster2 = await adminClient.createUser(poster2, '', '');
await adminClient.addToTeam(team.id, createdPoster2.id);
await adminClient.addToChannel(createdPoster2.id, created.id);
const {client: posterClient2} = await pw.makeClient({
username: poster2.username,
password: poster2.password,
});
if (!posterClient2) {
throw new Error('Failed to create second poster client');
}
// Post Spanish message that's long enough for reliable detection
await posterClient.createPost({
channel_id: created.id,
message: 'Este es un texto para la modal de traducción automática que debe ser lo suficientemente largo',
user_id: createdPoster.id,
});
// Second user posts a message so the first user's translation indicator appears
await posterClient2.createPost({
channel_id: created.id,
message: 'Segundo usuario con mensaje más largo para mejor detección de idioma',
user_id: createdPoster2.id,
});
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channelName);
await channelsPage.toBeVisible();
// Re-apply config + reload to counter concurrent initSetup() resets.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
await channelsPage.page.reload();
await channelsPage.toBeVisible();
// Post-reload re-apply: a concurrent initSetup() may have reset
// Enable during the ~500ms reload window.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
// * Wait for post by searching for the Spanish original (mock appends "[translated to en]", no real translation)
const modalPost = channelsPage.centerView.container
.locator('[id^="post_"]')
.filter({hasText: 'Este es un texto para la modal de traducción automática'});
await expect(modalPost).toBeVisible({timeout: 15000});
// * Check for translation button - if it exists, click it and verify modal
const translationButton = modalPost.getByRole('button', {name: 'This post has been translated'});
const hasTranslationButton = (await translationButton.count()) > 0;
// Translation button should be present - test expects translation to happen
if (!hasTranslationButton) {
throw new Error(
'Translation button not found on post. Expected autotranslation to produce a translated message indicator.',
);
}
// Translation happened - verify the modal opens
await translationButton.click();
const showTranslationDialog = page.getByRole('dialog').filter({hasText: 'Show Translation'});
await expect(showTranslationDialog).toBeVisible();
await expect(showTranslationDialog.getByText('ORIGINAL')).toBeVisible();
await expect(showTranslationDialog.getByText('AUTO-TRANSLATED')).toBeVisible();
},
);
test(
'message actions include Show translation',
{
tag: ['@autotranslation'],
},
async ({pw}) => {
const {adminClient, user, userClient, team} = await pw.initSetup();
const license = await adminClient.getClientLicenseOld();
test.skip(
!hasAutotranslationLicense(license.SkuShortName),
'Skipping test - server does not have Entry or Advanced license',
);
const translationUrl = process.env.TRANSLATION_SERVICE_URL || 'http://localhost:3010';
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
const channelName = `autotranslation-dotmenu-${pw.random.id()}`;
const created = await adminClient.createChannel({
team_id: team.id,
name: channelName,
display_name: 'Dot Menu Show Translation Test',
type: 'O',
});
await enableChannelAutotranslation(adminClient, created.id);
await adminClient.addToChannel(user.id, created.id);
await setUserChannelAutotranslation(userClient, created.id, true);
const poster = await pw.random.user('poster');
const createdPoster = await adminClient.createUser(poster, '', '');
await adminClient.addToTeam(team.id, createdPoster.id);
await adminClient.addToChannel(createdPoster.id, created.id);
const {client: posterClient} = await pw.makeClient({
username: poster.username,
password: poster.password,
});
if (!posterClient) {
throw new Error('Failed to create poster client');
}
// Create a second poster to show translation indicator (only visible with multiple users)
const poster2 = await pw.random.user('poster2');
const createdPoster2 = await adminClient.createUser(poster2, '', '');
await adminClient.addToTeam(team.id, createdPoster2.id);
await adminClient.addToChannel(createdPoster2.id, created.id);
const {client: posterClient2} = await pw.makeClient({
username: poster2.username,
password: poster2.password,
});
if (!posterClient2) {
throw new Error('Failed to create second poster client');
}
// Re-apply config immediately before posting so the server translates this message.
// A concurrent initSetup() can reset AutoTranslationSettings.Enable to false.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
// Confirm Enable=true before posting — translation happens at creation time,
// so the config must be confirmed before posts are submitted.
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
// Verify the mock translation service is reachable before attempting to use it.
// setMockSourceLanguage() swallows connection errors internally, so we probe the
// service directly. If it is not running, skip rather than fail — the service is
// an external dependency started by CI but not typically present in local runs.
try {
await fetch(translationUrl, {signal: AbortSignal.timeout(3000)});
} catch {
test.skip(
true,
`Mock translation service not reachable at ${translationUrl}. ` +
'Start the service or set TRANSLATION_SERVICE_URL to run this test.',
);
return;
}
// Set Spanish source so the mock returns source='es', triggering es→en translation.
await setMockSourceLanguage(translationUrl, 'es');
// Post Spanish message that's long enough for reliable detection.
await posterClient.createPost({
channel_id: created.id,
message: 'Este mensaje es para probar el menú de acciones con la opción de mostrar traducción automática',
user_id: createdPoster.id,
});
// Second post ensures the first post's translation indicator is rendered
// (the UI only renders it for posts that are not the last in the channel).
await posterClient2.createPost({
channel_id: created.id,
message: 'Segundo usuario con mensaje más largo para mejor detección de idioma',
user_id: createdPoster2.id,
});
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channelName);
await channelsPage.toBeVisible();
// Pre-reload re-apply: ensure Enable=true before the page loads so the client
// fetches posts with translations active.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
await channelsPage.page.reload();
await channelsPage.toBeVisible();
// Post-reload re-apply: a concurrent initSetup() on another shard may have reset
// Enable during the ~500ms reload window. Re-confirm before badge check.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
// * Wait for the channel-level autotranslation badge — confirms the feature is active.
await expect(channelsPage.centerView.autotranslationBadge).toBeVisible({timeout: 15000});
// * Wait for the translated target post to appear.
// The mock appends "[translated to en]" to the original Spanish text, confirming
// translation.state==='ready' so that "Show translation" will be in the dot menu.
const messagePost = channelsPage.centerView.container
.getByTestId('postView')
.filter({hasText: 'Este mensaje es para probar el menú de acciones'});
await expect(messagePost.getByText(/\[translated to en\]/i)).toBeVisible({timeout: 15000});
// * Open dot menu using the established hover → wait → click pattern
const post = new ChannelsPost(messagePost);
await post.hover();
await post.postMenu.toBeVisible();
await post.postMenu.dotMenuButton.click();
// Move mouse away so it doesn't hover over Remind and trigger its submenu.
// The submenu's MUI portal sets aria-hidden on the main menu, breaking getByRole.
await page.mouse.move(0, 0);
await channelsPage.postDotMenu.toBeVisible();
// * Verify the "Show translation" menu item is present
await expect(channelsPage.postDotMenu.showTranslationMenuItem).toBeVisible({timeout: 10000});
},
);
test(
'any user can disable and enable again autotranslation for themselves in a channel',
{
tag: ['@autotranslation'],
},
async ({pw}) => {
const {adminClient, user, userClient, team} = await pw.initSetup();
const license = await adminClient.getClientLicenseOld();
test.skip(
!hasAutotranslationLicense(license.SkuShortName),
'Skipping test - server does not have Entry or Advanced license',
);
const translationUrl = process.env.TRANSLATION_SERVICE_URL || 'http://localhost:3010';
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
const channelName = `autotranslation-toggle-${pw.random.id()}`;
const created = await adminClient.createChannel({
team_id: team.id,
name: channelName,
display_name: 'Autotranslation Toggle Test',
type: 'O',
});
await enableChannelAutotranslation(adminClient, created.id);
await adminClient.addToChannel(user.id, created.id);
await setUserChannelAutotranslation(userClient, created.id, true);
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channelName);
await channelsPage.toBeVisible();
// Defeat concurrent initSetup() config resets: re-apply on every poll iteration until badge appears.
await expect
.poll(
async () => {
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
return channelsPage.centerView.autotranslationBadge.isVisible();
},
{timeout: 45000, intervals: [2000]},
)
.toBeTruthy();
await channelsPage.centerView.header.openChannelMenu();
await page.getByRole('menuitem', {name: 'Disable autotranslation'}).click();
await page.getByRole('button', {name: 'Turn off auto-translation'}).click();
await expect(channelsPage.centerView.autotranslationBadge).not.toBeVisible();
// Re-apply config before opening menu to ensure "Enable autotranslation" option is present.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
await channelsPage.centerView.header.openChannelMenu();
await page.getByRole('menuitem', {name: 'Enable autotranslation'}).click();
// Poll with config re-apply until badge reappears.
await expect
.poll(
async () => {
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
return channelsPage.centerView.autotranslationBadge.isVisible();
},
{timeout: 45000, intervals: [2000]},
)
.toBeTruthy();
},
);
test(
'autotranslation badge is only visible on translated channels',
{
tag: ['@autotranslation'],
},
async ({pw}) => {
const {adminClient, user, userClient, team} = await pw.initSetup();
const license = await adminClient.getClientLicenseOld();
test.skip(
!hasAutotranslationLicense(license.SkuShortName),
'Skipping test - server does not have Entry or Advanced license',
);
const translationUrl = process.env.TRANSLATION_SERVICE_URL || 'http://localhost:3010';
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
const translatedChannelName = `autotranslation-badge-${pw.random.id()}`;
const translatedChannel = await adminClient.createChannel({
team_id: team.id,
name: translatedChannelName,
display_name: 'Translated Channel',
type: 'O',
});
await enableChannelAutotranslation(adminClient, translatedChannel.id);
await adminClient.addToChannel(user.id, translatedChannel.id);
await setUserChannelAutotranslation(userClient, translatedChannel.id, true);
const noTranslationChannelName = `no-translation-${pw.random.id()}`;
const noTranslationChannel = await adminClient.createChannel({
team_id: team.id,
name: noTranslationChannelName,
display_name: 'No Translation Channel',
type: 'O',
});
await adminClient.addToChannel(user.id, noTranslationChannel.id);
const {channelsPage} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, translatedChannelName);
await channelsPage.toBeVisible();
// Re-apply config + reload to counter concurrent initSetup() resets.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
await channelsPage.page.reload();
// Post-reload re-apply: firing the CONFIG_CHANGED WebSocket event during page load
// rather than after prevents it from disrupting the badge render.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
await channelsPage.toBeVisible();
await expect(channelsPage.centerView.autotranslationBadge).toBeVisible({timeout: 15000});
await channelsPage.goto(team.name, noTranslationChannelName);
await channelsPage.toBeVisible();
await expect(channelsPage.centerView.autotranslationBadge).not.toBeVisible();
},
);
test(
'unsupported language does not show channel badge and shows message in channel header menu',
{
tag: ['@autotranslation'],
},
async ({pw}) => {
const {adminClient, user, userClient, team} = await pw.initSetup();
const license = await adminClient.getClientLicenseOld();
test.skip(
!hasAutotranslationLicense(license.SkuShortName),
'Skipping test - server does not have Entry or Advanced license',
);
const translationUrl = process.env.TRANSLATION_SERVICE_URL || 'http://localhost:3010';
await enableAutotranslationConfig(adminClient, {
mockBaseUrl: translationUrl,
targetLanguages: ['en', 'es'],
});
const channelName = `autotranslation-unsupported-${pw.random.id()}`;
const created = await adminClient.createChannel({
team_id: team.id,
name: channelName,
display_name: 'Unsupported Language Test',
type: 'O',
});
await enableChannelAutotranslation(adminClient, created.id);
await adminClient.addToChannel(user.id, created.id);
await userClient.patchMe({locale: 'fr'});
const {channelsPage, page} = await pw.testBrowser.login(user);
await channelsPage.goto(team.name, channelName);
await channelsPage.toBeVisible();
// Re-apply config + reload to ensure the browser reads the latest AutoTranslationSettings.
// The badge should still be absent (French locale is not in targetLanguages), but the
// server config must be active so the channel header menu shows the "unsupported" notice.
await enableAutotranslationConfig(adminClient, {mockBaseUrl: translationUrl, targetLanguages: ['en', 'es']});
await pw.waitUntil(async () => {
const cfg = await adminClient.getConfig();
return (cfg as any).AutoTranslationSettings?.Enable === true;
});
await channelsPage.page.reload();
await channelsPage.toBeVisible();
await expect(channelsPage.centerView.autotranslationBadge).not.toBeVisible({timeout: 30000});
await channelsPage.centerView.header.openChannelMenu();
const channelMenu = page
.getByRole('menu')
.filter({has: page.getByRole('menuitem', {name: /Auto-translation|Channel Settings/})});
await expect(channelMenu.getByText('Auto-translation', {exact: true})).toBeVisible({timeout: 30000});
await expect(channelMenu.getByText('Your language is not supported')).toBeVisible({timeout: 30000});
const autotranslationItem = page.getByRole('menuitem', {name: /Auto-translation/});
await expect(autotranslationItem).toBeDisabled();
},
);