Skip to content
Open
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
96 changes: 73 additions & 23 deletions test/e2e/playwright/helpers/file-manager-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
* ```
*/

import * as fs from 'fs';
import * as path from 'path';
import type { Page, Locator } from '@playwright/test';
import { createTestFileWithName } from './special-chars-helpers';

// ═══════════════════════════════════════════════════════════════════════════════
// FILE MANAGER MODAL
Expand Down Expand Up @@ -85,10 +86,36 @@ export async function isFileManagerOpen(page: Page): Promise<boolean> {
// UPLOAD OPERATIONS
// ═══════════════════════════════════════════════════════════════════════════════

// Cache for fixture image buffer (loaded once per test run)
let fixtureImageBuffer: Buffer | null = null;

function getFixtureBuffer(): Buffer {
if (!fixtureImageBuffer) {
fixtureImageBuffer = fs.readFileSync(path.join(process.cwd(), 'test/fixtures/sample-2.jpg'));
}
return fixtureImageBuffer;
}

/**
* Build a unique buffer for the given filename.
*
* AssetManager deduplicates uploads by content hash — uploading the same bytes
* under different filenames silently skips the second upload and the grid never
* updates. Appending a filename-derived marker after the JPEG EOI marker gives
* every upload a distinct hash while keeping the image valid (JPEG readers stop
* at EOI and ignore trailing data).
*/
function makeUniqueBuffer(filename: string): Buffer {
const base = getFixtureBuffer();
const marker = Buffer.from(`\x00${filename}`);
return Buffer.concat([base, marker]);
}

/**
* Upload a file with a specific filename (handles unicode/special characters).
* Uses DataTransfer API to set files with arbitrary names since Playwright's
* setInputFiles only accepts actual file paths.
* Uses Playwright's setInputFiles with a per-filename unique buffer so that
* AssetManager content-hash deduplication does not silently drop subsequent
* uploads of the same fixture image.
*
* @param page - Playwright page
* @param filename - The filename to use for the upload
Expand All @@ -97,18 +124,13 @@ export async function uploadFileWithSpecialName(page: Page, filename: string): P
// Get initial file count to verify upload
const initialCount = await page.locator('#modalFileManager .media-library-item:not(.media-library-folder)').count();

// Create the test file with the special name
await createTestFileWithName(page, filename);

// Use DataTransfer to set the file on the input within the modal
await page.evaluate(() => {
const input = document.querySelector('#modalFileManager .media-library-upload-input') as HTMLInputElement;
if (!input) throw new Error('Upload input not found');
const ext = filename.split('.').pop()?.toLowerCase() || 'jpg';
const mimeType = ext === 'png' ? 'image/png' : 'image/jpeg';

const dt = new DataTransfer();
dt.items.add((window as any).__testFile);
input.files = dt.files;
input.dispatchEvent(new Event('change', { bubbles: true }));
await page.locator('#modalFileManager .media-library-upload-input').setInputFiles({
name: filename,
mimeType,
buffer: makeUniqueBuffer(filename),
});

// Wait for file count to increase (more reliable than checking specific filename)
Expand Down Expand Up @@ -501,6 +523,13 @@ export async function navigateToFolder(page: Page, folderName: string): Promise<
folderName,
{ timeout: 10000 },
);

// Wait for grid to finish loading (ensures loadFolderContents is complete before uploading)
await page
.waitForFunction(() => !document.querySelector('#modalFileManager .media-library-loading'), undefined, {
timeout: 10000,
})
.catch(() => {});
}

/**
Expand All @@ -522,6 +551,13 @@ export async function navigateToRoot(page: Page): Promise<void> {
undefined,
{ timeout: 10000 },
);

// Wait for grid to finish loading after navigating to root
await page
.waitForFunction(() => !document.querySelector('#modalFileManager .media-library-loading'), undefined, {
timeout: 10000,
})
.catch(() => {});
}

/**
Expand Down Expand Up @@ -684,6 +720,13 @@ export async function clearSearch(page: Page): Promise<void> {
undefined,
{ timeout: 5000 },
);

// Wait for grid to reload after clearing search (leaves search mode)
await page
.waitForFunction(() => !document.querySelector('#modalFileManager .media-library-loading'), undefined, {
timeout: 10000,
})
.catch(() => {});
}

// ═══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -778,15 +821,22 @@ export async function insertFileIntoEditor(page: Page, filename: string): Promis
await altTextField.fill('Test image');
}

// Click Save in TinyMCE dialog
const saveBtn = page.locator('.tox-dialog .tox-button[title="Save"], .tox-dialog .tox-button:has-text("Save")');
if (
await saveBtn
.first()
.isVisible()
.catch(() => false)
) {
await saveBtn.first().click();
// Click the primary (Save/Ok/Guardar) button in TinyMCE dialog.
// Use a language-agnostic selector: the primary button is the one WITHOUT the
// tox-button--secondary or tox-button--naked modifiers.
const tinyMceDialog = page.locator('.tox-dialog');
if (await tinyMceDialog.isVisible().catch(() => false)) {
const primaryBtn = tinyMceDialog
.locator('.tox-button:not(.tox-button--secondary):not(.tox-button--naked)')
.first();
if (await primaryBtn.isVisible().catch(() => false)) {
await primaryBtn.click();
} else {
// Fallback: press Enter to submit the dialog
await page.keyboard.press('Enter');
}
// Wait for TinyMCE dialog to close
await tinyMceDialog.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
}

await page.waitForFunction(
Expand Down
26 changes: 16 additions & 10 deletions test/e2e/playwright/helpers/workarea-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2231,14 +2231,19 @@ export async function addTextIdeviceWithContent(page: Page, content: string): Pr
* @returns The Download object for the exported file
*/
export async function downloadProject(page: Page): Promise<Download> {
// Close any open dialogs or modals first
// Close any open TinyMCE dialogs — use language-agnostic selector (Cancel =
// secondary button) rather than text-based, since locale may differ.
const tinyMceDialog = page.locator('.tox-dialog');
if (await tinyMceDialog.isVisible().catch(() => false)) {
const cancelBtn = page.locator('.tox-dialog .tox-button:has-text("Cancel")');
const cancelBtn = tinyMceDialog
.locator('.tox-button--secondary, .tox-button:has-text("Cancel"), .tox-button:has-text("Cancelar")')
.first();
if (await cancelBtn.isVisible().catch(() => false)) {
await cancelBtn.click();
await page.waitForTimeout(300);
} else {
await page.keyboard.press('Escape');
}
await tinyMceDialog.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
}

const fileManagerModal = page.locator('#modalFileManager');
Expand All @@ -2252,22 +2257,23 @@ export async function downloadProject(page: Page): Promise<Download> {

await dismissBlockingAlertModal(page);

// Detect static mode (no remote storage capability)
const isStaticMode = await page.evaluate(() => {
const capabilities = (window as any).eXeLearning?.app?.capabilities;
return capabilities && !capabilities.storage?.remote;
// Only Electron/offline-installation mode uses the save button for download.
// Static web AND online modes both use the File menu path which calls
// downloadProjectViaYjs() — a fully client-side ELPX export.
const isOfflineInstallation = await page.evaluate(() => {
return (window as any).eXeLearning?.config?.isOfflineInstallation === true;
});

const triggerDownload = async (): Promise<void> => {
if (isStaticMode) {
// STATIC MODE: The save button triggers download in offline mode
if (isOfflineInstallation) {
// ELECTRON/OFFLINE MODE: The save button triggers download directly
const saveBtn = page.locator('#head-top-save-button');
await saveBtn.waitFor({ state: 'visible', timeout: 5000 });
await saveBtn.click();
return;
}

// ONLINE MODE: Navigate through File menu dropdown
// ONLINE AND STATIC WEB MODE: Navigate through File menu dropdown
await page.evaluate(() => {
document.querySelector('body')?.setAttribute('mode', 'advanced');
});
Expand Down
Loading
Loading