Skip to content
Merged
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
171 changes: 121 additions & 50 deletions e2e/specs/mattermost/media_preview.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {prepareMattermostServerView} from '../../helpers/prepareServerView';
import {getFilePublicLink, isPublicLinkEnabled} from '../../helpers/server_api/publicLinks';
import type {ServerView} from '../../helpers/serverView';

// 64x64 PNG — above Mattermost's 48px inline-image minimum so thumbnails render visibly.
const PREVIEW_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAf0lEQVR4nNXOQREAIAzAsFJJ8y8FMYjgsWsU5NwZyiRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4iRO4twO/HqSogHAzFmDswAAAABJRU5ErkJggg==';
// Valid 128x128 PNG. The previous fixture was rejected by the server decoder
// ("png: invalid format: too much pixel data"), so no preview was generated and
// SizeAwareImage (MM-69174 / 11.10+) ignored clicks until load forever.
const PREVIEW_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAIAAABMXPacAAACx0lEQVR4nO3dsVEbQRhH8ZWHOuyAHogcEVAClTgmcGpX4hIIlNgRPRC4EjnYmRuNYARrab/3P/R+kQMh43333Z1kFm12u10b8eXnn6HH//321ec/4tPQo3V2BoAZAGYAmAFgBoAZAGYAmAFgBoAZAGYAmAFgBoAZAHaV9v74pT2/EwAzAMwAMAPADAAzAMwAMAPADAAzAMwAMAPADAAzAMwAsM3q9gdsf30//oDrp8dTnv/A7P8/uBp6NOXNRd/3fHO3/Pl4jATRAYbW/VVLjNgSoQFOX/oDvURghrgAZ1/6fc83d9vWbu8f5v0Vo7LugvZP3/NMbTwqZQJqln7RGySMQsQEFK/+ImEU+ADU6nd4AzgAu/od24AMkLD6HdgAC5Cz+h3VgAmQtvod0gAIkLn6XX0D/i7owlUHSD78u+Ih2Hz+8XvoC055fxy/6X6/5W079wd8cHUBVnT4t8JTpRMAKwqwrsO/qxkCJwBmAFhFgDWef7qCs5ATADMAzACw6QHWewHoZn//TgDMADADwAwAm74/YO0X4Tb4I71+fsDKGABmAJgBYAaATQ8QuCtoyOzv3wmAGQBmAFhFgPVeBgo2kTkBMAPAigKs8SxUs4nVCYDVBVjXEJTt4S79/IDt0Feiln/mh9ofkPCrAd6jclirrwH5DYpPlV6EYUCA5CGov1NgJiCzAXKfhp2C0hpQd8nkNSCnAfgaBb4IJzRgXyHyd0FsA/z1OR+gcQ3w1W85v7SvNyj7OcaEpe8iJmBRMwo5q9/SArTWrp8e5y3Q7f1D1Oq3nFPQgb5MZ9wlmnC79arQAN1ytP53idh1X/j5AW/w8wMOHRzUowuUJu4ifGkMADMAzAAwA8AMADMAzAAwA8AMADMAzAAwA8AMACvdH+Dzv+QEwAwAMwDMADADwAwAMwDMADADwAwAMwDMADADwAwAMwDsH5fw4xGXqkx/AAAAAElFTkSuQmCC';

const PREVIEW_MODAL_SELECTOR = [
'.file-preview-modal',
Expand All @@ -19,17 +21,88 @@ const PREVIEW_MODAL_SELECTOR = [
'#viewImageModalLabel',
].join(', ');

const POSTED_IMAGE_SELECTOR = [
'.post-image .small-image__container',
'.post-image .image-loaded-container',
'.post-image__image',
'.post-image img',
'.file-viewer-touch',
'.file-attachment',
'.post--attachment img',
'img[src*="/api/v4/files/"]',
const LOADED_IMAGE_SELECTOR = [
'.post-image img:not(.image-loading__placeholder)',
'.post--attachment img:not(.image-loading__placeholder)',
'img[src*="/api/v4/files/"]:not(.image-loading__placeholder)',
].join(', ');

const PREVIEW_FILE_NAME = 'e2e-preview.png';

// Shared helpers injected into renderer scripts (same pattern as DOM_UTILS in serverView.ts).
const PREVIEW_IMAGE_UTILS = `
const LOADED_IMAGE_SELECTOR = ${JSON.stringify(LOADED_IMAGE_SELECTOR)};
const PREVIEW_FILE_NAME = ${JSON.stringify(PREVIEW_FILE_NAME)};
const isPreviewControlVisible = (el) => el instanceof HTMLElement && window.getComputedStyle(el).display !== 'none';
const isLoadedPreviewImage = (el) => el instanceof HTMLImageElement &&
!el.classList.contains('image-loading__placeholder') &&
el.complete &&
el.naturalWidth > 0 &&
isPreviewControlVisible(el);
const postHasPreviewFixture = (post) => {
if (post.querySelector('[aria-label*="' + PREVIEW_FILE_NAME + '" i]')) {
return true;
}
// Filename can also appear in attachment headers before the image aria-label mounts.
const attachment = post.querySelector('.post-image, .post--attachment, .file-attachment, .file-preview__button');
return Boolean(attachment && (attachment.textContent || '').toLowerCase().includes(PREVIEW_FILE_NAME));
};
const findPreviewFixturePost = () => {
const posts = Array.from(document.querySelectorAll('.post'));
for (let index = posts.length - 1; index >= 0; index--) {
const post = posts[index];
if (postHasPreviewFixture(post)) {
return post;
}
}
return null;
};
const findVisibleLoadedPreviewButton = (root) => {
for (const button of root.querySelectorAll('.file-preview__button')) {
if (!isPreviewControlVisible(button)) {
continue;
}
const loadedImg = button.querySelector('img:not(.image-loading__placeholder)');
if (loadedImg instanceof HTMLImageElement && loadedImg.complete && loadedImg.naturalWidth > 0) {
return button;
}
}
return null;
};
`;

/**
* Mattermost 11.10+ (MM-69174) SizeAwareImage ignores clicks until the real image has
* loaded, and keeps a visible placeholder button while the clickable control is
* display:none. Wait for a visible, loaded non-placeholder control before opening.
*/
async function waitForLoadedImagePreviewControl(serverWin: ServerView): Promise<void> {
await expect.poll(async () => serverWin.runInRenderer<boolean>(`
${PREVIEW_IMAGE_UTILS}
const post = findPreviewFixturePost();
if (!post) {
return false;
}

const previewButton = findVisibleLoadedPreviewButton(post);
if (previewButton) {
previewButton.scrollIntoView({block: 'center'});
return true;
}

// Legacy servers without .file-preview__button
const legacyImg = post.querySelector(LOADED_IMAGE_SELECTOR);
if (isLoadedPreviewImage(legacyImg)) {
legacyImg.scrollIntoView({block: 'center'});
return true;
}
return false;
`, true), {
timeout: 60_000,
message: 'Uploaded e2e-preview.png must finish loading into a visible file-preview control before it can be opened',
}).toBe(true);
}

async function submitComposerPost(serverWin: ServerView): Promise<void> {
const sent = await serverWin.runInRenderer<boolean>(`
const sendButton = document.querySelector(
Expand All @@ -49,24 +122,20 @@ async function submitComposerPost(serverWin: ServerView): Promise<void> {

async function waitForPostedAttachment(serverWin: ServerView): Promise<void> {
await expect.poll(async () => serverWin.runInRenderer<boolean>(`
const attachmentSelector = ${JSON.stringify(POSTED_IMAGE_SELECTOR)};
${PREVIEW_IMAGE_UTILS}
const composer = document.querySelector('#post-create, .AdvancedTextEditor, .post-create, [data-testid="post-create"]');
const draftAttachment = composer?.querySelector('.file-preview, .file-preview__container, .attachment-preview');
if (draftAttachment) {
return false;
}

const posts = Array.from(document.querySelectorAll('.post'));
for (let index = posts.length - 1; index >= 0; index--) {
const post = posts[index];
if (post.querySelector(attachmentSelector) ||
post.querySelector('[aria-label*="e2e-preview.png" i], [aria-label*="file thumbnail" i]')) {
post.scrollIntoView({block: 'center'});
return true;
}
const post = findPreviewFixturePost();
if (!post) {
return false;
}
return false;
`, true), {timeout: 60_000, message: 'Uploaded image must appear in the channel post list'}).toBe(true);
post.scrollIntoView({block: 'center'});
return true;
`, true), {timeout: 60_000, message: 'Uploaded e2e-preview.png must appear in the channel post list'}).toBe(true);
}

async function uploadAndPostPng(serverWin: ServerView): Promise<void> {
Expand All @@ -77,7 +146,7 @@ async function uploadAndPostPng(serverWin: ServerView): Promise<void> {
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
const file = new File([bytes], 'e2e-preview.png', {type: 'image/png'});
const file = new File([bytes], ${JSON.stringify(PREVIEW_FILE_NAME)}, {type: 'image/png'});

const input = document.querySelector('#fileUploadInput, input[type="file"]');
if (!(input instanceof HTMLInputElement)) {
Expand Down Expand Up @@ -109,6 +178,7 @@ async function uploadAndPostPng(serverWin: ServerView): Promise<void> {
await recoverInteractiveChannel(serverWin, {channelItem: '#sidebarItem_town-square'});

await waitForPostedAttachment(serverWin);
await waitForLoadedImagePreviewControl(serverWin);
}

async function isImagePreviewOpen(serverWin: ServerView): Promise<boolean> {
Expand All @@ -126,45 +196,46 @@ async function isImagePreviewOpen(serverWin: ServerView): Promise<boolean> {

async function openImagePreview(serverWin: ServerView): Promise<boolean> {
return serverWin.runInRenderer<boolean>(`
const attachmentSelector = ${JSON.stringify(POSTED_IMAGE_SELECTOR)};
const posts = Array.from(document.querySelectorAll('.post'));
let root = null;
for (let index = posts.length - 1; index >= 0; index--) {
const post = posts[index];
if (post.querySelector(attachmentSelector) ||
post.querySelector('[aria-label*="e2e-preview.png" i], [aria-label*="file thumbnail" i]')) {
root = post;
break;
}
}
${PREVIEW_IMAGE_UTILS}
const root = findPreviewFixturePost();
if (!root) {
return false;
}

// Prefer the visible SizeAwareImage control (11.10+/MM-69174); clicks on the
// placeholder button are intentionally ignored until the real image loads.
const previewButton = findVisibleLoadedPreviewButton(root);
if (previewButton) {
previewButton.scrollIntoView({block: 'center', inline: 'center'});
previewButton.click();
return true;
}

const clickTargets = [
root.querySelector('[aria-label*="e2e-preview.png" i]'),
root.querySelector('[aria-label*="file thumbnail" i]'),
root.querySelector('.post-image .small-image__container'),
...Array.from(root.querySelectorAll('[aria-label*="' + PREVIEW_FILE_NAME + '" i]')),
...Array.from(root.querySelectorAll(LOADED_IMAGE_SELECTOR)),
root.querySelector('.post-image .image-loaded-container'),
root.querySelector('.post-image .small-image__container'),
root.querySelector('.post-image__image'),
root.querySelector('.post-image img'),
root.querySelector('.file-viewer-touch'),
root.querySelector('.file-attachment'),
root.querySelector('.post--attachment img'),
root.querySelector('img[src*="/api/v4/files/"]'),
root.querySelector('.post-image'),
root.querySelector('.post--attachment'),
].filter(Boolean);
].filter((target) => {
if (!target) {
return false;
}
if (target instanceof HTMLImageElement) {
return isLoadedPreviewImage(target);
}
return isPreviewControlVisible(target) &&
Boolean(target.querySelector?.('img:not(.image-loading__placeholder)'));
});

const target = clickTargets[0];
if (!target) {
if (!(target instanceof HTMLElement)) {
return false;
}

target.scrollIntoView({block: 'center', inline: 'center'});
if (target instanceof HTMLElement) {
target.click();
}
target.click();
return true;
`, true);
}
Expand All @@ -185,8 +256,8 @@ async function getPreviewFileId(serverWin: ServerView): Promise<string | null> {
const sources = [
document.querySelector('[data-testid="imagePreview"]')?.getAttribute('src'),
document.querySelector('.file-preview-modal img')?.getAttribute('src'),
document.querySelector('.post-image img[src*="/files/"]')?.getAttribute('src'),
document.querySelector('img[src*="/api/v4/files/"]')?.getAttribute('src'),
document.querySelector('.post-image img[src*="/files/"]:not(.image-loading__placeholder)')?.getAttribute('src'),
document.querySelector('img[src*="/api/v4/files/"]:not(.image-loading__placeholder)')?.getAttribute('src'),
].filter(Boolean);

for (const source of sources) {
Expand Down
Loading