diff --git a/test/e2e/playwright/helpers/file-manager-helpers.ts b/test/e2e/playwright/helpers/file-manager-helpers.ts index ec26ef091..25707ad09 100644 --- a/test/e2e/playwright/helpers/file-manager-helpers.ts +++ b/test/e2e/playwright/helpers/file-manager-helpers.ts @@ -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 @@ -85,10 +86,36 @@ export async function isFileManagerOpen(page: Page): Promise { // 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 @@ -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) @@ -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(() => {}); } /** @@ -522,6 +551,13 @@ export async function navigateToRoot(page: Page): Promise { 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(() => {}); } /** @@ -684,6 +720,13 @@ export async function clearSearch(page: Page): Promise { 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(() => {}); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -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( diff --git a/test/e2e/playwright/helpers/workarea-helpers.ts b/test/e2e/playwright/helpers/workarea-helpers.ts index 0d2c33926..8820f1adc 100644 --- a/test/e2e/playwright/helpers/workarea-helpers.ts +++ b/test/e2e/playwright/helpers/workarea-helpers.ts @@ -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 { - // 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'); @@ -2252,22 +2257,23 @@ export async function downloadProject(page: Page): Promise { 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 => { - 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'); }); diff --git a/test/e2e/playwright/specs/file-manager-special-chars.spec.ts b/test/e2e/playwright/specs/file-manager-special-chars.spec.ts index e268edd0b..aad9b5603 100644 --- a/test/e2e/playwright/specs/file-manager-special-chars.spec.ts +++ b/test/e2e/playwright/specs/file-manager-special-chars.spec.ts @@ -26,6 +26,8 @@ import { zipContainsFile, openElpFile, dismissBlockingAlertModal, + selectFirstPage, + addTextIdevice, } from '../helpers/workarea-helpers'; import { uploadFileWithSpecialName, @@ -58,89 +60,14 @@ import type { Page } from '@playwright/test'; // ═══════════════════════════════════════════════════════════════════════════════ /** - * Helper to add a text iDevice and enter edit mode to access TinyMCE image dialog - */ -async function addTextIdeviceFromPanel(page: Page): Promise { - // Select a page in the navigation tree - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - await page.waitForTimeout(500); - - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => {}); - - // Try quick access button first - const quickTextButton = page - .locator('[data-testid="quick-idevice-text"], .quick-idevice-btn[data-idevice="text"]') - .first(); - if ((await quickTextButton.count()) > 0 && (await quickTextButton.isVisible())) { - await quickTextButton.click(); - } else { - // Expand "Information and presentation" category - const infoCategory = page - .locator('#menu_idevices .accordion-item') - .filter({ hasText: /Information|Información/i }) - .locator('.accordion-button'); - - if ((await infoCategory.count()) > 0) { - const isCollapsed = await infoCategory.first().evaluate(el => el.classList.contains('collapsed')); - if (isCollapsed) { - await infoCategory.first().click(); - await page.waitForTimeout(500); - } - } - - const textIdevice = page.locator('.idevice_item[id="text"], [data-testid="idevice-text"]').first(); - await textIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await textIdevice.click(); - } - - await page.locator('#node-content article .idevice_node.text').first().waitFor({ timeout: 15000 }); -} - -/** - * Helper to open the File Manager modal via TinyMCE image dialog + * Helper to open the File Manager modal via TinyMCE image dialog. * This is specific to the special chars tests that need TinyMCE context. */ async function openFileManagerViaTinyMCE(page: Page): Promise { const textBlock = page.locator('#node-content article .idevice_node.text').last(); if ((await textBlock.count()) === 0) { - await addTextIdeviceFromPanel(page); + await selectFirstPage(page); + await addTextIdevice(page); } const activeTextBlock = page.locator('#node-content article .idevice_node.text').last(); @@ -211,424 +138,109 @@ async function openFileManagerViaTinyMCE(page: Page): Promise { await page.waitForSelector('#modalFileManager[data-open="true"], #modalFileManager.show', { timeout: 10000 }); } +/** + * Helper to save the text iDevice after editing. + */ +async function saveTextIdevice(page: Page): Promise { + const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); + if (await saveIdeviceBtn.isVisible().catch(() => false)) { + await saveIdeviceBtn.click(); + } +} + // ═══════════════════════════════════════════════════════════════════════════════ // TEST SUITES // ═══════════════════════════════════════════════════════════════════════════════ test.describe('File Manager - Special Characters', () => { - test.describe('Upload Operations - Unicode Languages', () => { - test('should upload file with Spanish characters (archivo_espanol.jpg)', async ({ + test.describe('Upload Operations', () => { + test('should upload files with CJK and Latin special characters', async ({ authenticatedPage, createProject, }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Spanish'); + const projectUuid = await createProject(page, 'Upload - Unicode Languages'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - const filename = getUniqueTestFilename('archivo_espanol.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); + const filenames = [ + getUniqueTestFilename('archivo_espanol.jpg'), + getUniqueTestFilename('中文文件.jpg'), + getUniqueTestFilename('日本語ファイル.jpg'), + getUniqueTestFilename('한국어파일.jpg'), + getUniqueTestFilename('Test_日本語_中文.jpg'), + ]; - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project - await saveProject(page); - }); - - test('should upload file with Chinese characters (中文文件.jpg)', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Chinese'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); + // Open file manager once and upload all files await openFileManagerViaTinyMCE(page); - - const filename = getUniqueTestFilename('中文文件.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); + for (const filename of filenames) { + await uploadFileWithSpecialName(page, filename); + await waitForFileInGrid(page, filename); + expect(await verifyFileWithThumbnail(page, filename)).toBe(true); } - // Save project - await saveProject(page); - }); - - test('should upload file with Japanese characters (日本語ファイル.jpg)', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Japanese'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const filename = getUniqueTestFilename('日本語ファイル.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } + // Insert one representative file into editor and verify + const representative = filenames[1]; // Chinese filename + await insertFileIntoEditor(page, representative); + expect(await verifyImageInEditor(page, representative)).toBe(true); - // Save project + await saveTextIdevice(page); await saveProject(page); }); - test('should upload file with Korean characters (한국어파일.jpg)', async ({ + test('should upload files with spaces and ASCII special characters', async ({ authenticatedPage, createProject, }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Korean'); + const projectUuid = await createProject(page, 'Upload - ASCII Special Chars'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const filename = getUniqueTestFilename('한국어파일.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - // Insert into editor - await insertFileIntoEditor(page, filename); + const ts = Date.now(); + const filenames = [ + `file with spaces ${ts}.jpg`, + `file with multiple spaces_${ts}.jpg`, + `file_with-underscores_and-hyphens_${ts}.jpg`, + `file_123_test_${ts}.jpg`, + ]; - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project - await saveProject(page); - }); - - test('should upload file with mixed unicode (Test_日本語_中文.jpg)', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Mixed Unicode'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - - const filename = getUniqueTestFilename('Test_日本語_中文.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project - await saveProject(page); - }); - }); - - test.describe('Upload Operations - Spaces and Symbols', () => { - test('should upload file with spaces in name', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Spaces'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const filename = `file with spaces ${Date.now()}.jpg`; - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); + for (const filename of filenames) { + await uploadFileWithSpecialName(page, filename); + await waitForFileInGrid(page, filename); + expect(await verifyFileWithThumbnail(page, filename)).toBe(true); } - // Save project - await saveProject(page); - }); + // Insert one representative file and verify + await insertFileIntoEditor(page, filenames[0]); + expect(await verifyImageInEditor(page, filenames[0])).toBe(true); - test('should upload file with multiple consecutive spaces', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Multiple Spaces'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const filename = `file with multiple spaces_${Date.now()}.jpg`; - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project + await saveTextIdevice(page); await saveProject(page); }); - test('should upload file with underscores and hyphens', async ({ authenticatedPage, createProject }) => { + test('should upload files with edge case names', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Underscores Hyphens'); + const projectUuid = await createProject(page, 'Upload - Edge Cases'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const filename = `file_with-underscores_and-hyphens_${Date.now()}.jpg`; - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); + const ts = Date.now(); + const filenames = [`multiple.dots.file.${ts}.jpg`, 'a.jpg', getUniqueTestFilename('naive_resume.jpg')]; - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project - await saveProject(page); - }); - - test('should upload file with numbers and symbols', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Numbers Symbols'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - - const filename = `file_123_test_${Date.now()}.jpg`; - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); + for (const filename of filenames) { + await uploadFileWithSpecialName(page, filename); + await waitForFileInGrid(page, filename); + expect(await verifyFileWithThumbnail(page, filename)).toBe(true); } - // Save project - await saveProject(page); - }); - }); - - test.describe('Upload Operations - Edge Cases', () => { - test('should upload file with multiple dots in name', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Multiple Dots'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const filename = `multiple.dots.file.${Date.now()}.jpg`; - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); + // Insert one representative file and verify + await insertFileIntoEditor(page, filenames[0]); + expect(await verifyImageInEditor(page, filenames[0])).toBe(true); - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project - await saveProject(page); - }); - - test('should upload file with very short name (a.jpg)', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Short Name'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const filename = 'a.jpg'; - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project - await saveProject(page); - }); - - test('should upload file with diacritics (naïve_résumé.jpg)', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Special Chars - Diacritics'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const filename = getUniqueTestFilename('naive_resume.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Verify in file manager grid - await waitForFileInGrid(page, filename); - const hasValidThumbnail = await verifyFileWithThumbnail(page, filename); - expect(hasValidThumbnail).toBe(true); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project + await saveTextIdevice(page); await saveProject(page); }); @@ -644,571 +256,280 @@ test.describe('File Manager - Special Characters', () => { await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload two files with longer delays const filename1 = getUniqueTestFilename('archivo_espanol.jpg'); const filename2 = getUniqueTestFilename('中文文件.jpg'); - // First file - await uploadFileWithSpecialName(page, filename1); - await waitForFileInGrid(page, filename1); - await page.waitForTimeout(500); - - // Second file - await uploadFileWithSpecialName(page, filename2); - await waitForFileInGrid(page, filename2); - - // Verify both files are present - const allFiles = await getAllFilenames(page); - expect(allFiles).toContain(filename1); - expect(allFiles).toContain(filename2); - }); - }); - - test.describe('Folder Operations', () => { - test('should create folder with valid alphanumeric name', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Folder - Alphanumeric'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const folderName = `TestFolder_${Date.now()}`; - const success = await createFolderWithName(page, folderName); - - expect(success).toBe(true); - const exists = await folderExists(page, folderName); - expect(exists).toBe(true); - }); - - test('should create folder with hyphen and underscore', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Folder - Hyphen Underscore'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - const folderName = `My-Folder_Name_${Date.now()}`; - const success = await createFolderWithName(page, folderName); - - expect(success).toBe(true); - const exists = await folderExists(page, folderName); - expect(exists).toBe(true); - }); - - test('should handle unicode folder name (sanitized or rejected)', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Folder - Unicode'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Note: Folder names are restricted to [a-zA-Z0-9\-_./] - // Unicode characters should be sanitized or rejected - const folderName = '日本語フォルダ'; - const success = await createFolderWithName(page, folderName); - - // Either folder creation fails or name is sanitized - // We just verify the operation doesn't crash - const folderCount = await getFolderCount(page); - expect(folderCount).toBeGreaterThanOrEqual(0); - - // If folder was created, verify it doesn't have the unicode name literally - if (success) { - // The name may have been sanitized - console.log('Folder was created (possibly sanitized)'); - } else { - console.log('Folder creation rejected (unicode not allowed)'); - } - }); - - test('should navigate into folder and back using breadcrumbs', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Folder - Navigation'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Create folder - const folderName = `NavFolder_${Date.now()}`; - await createFolderWithName(page, folderName); - - // Navigate into folder - await navigateToFolder(page, folderName); - - // Verify breadcrumbs show folder - const breadcrumbs = page.locator('#modalFileManager .media-library-breadcrumbs'); - await expect(breadcrumbs).toContainText(folderName); - - // Navigate back to root - await navigateToRoot(page); - - // Verify folder is visible again - const exists = await folderExists(page, folderName); - expect(exists).toBe(true); - }); - - test('should upload unicode file inside folder', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Folder - Upload Unicode'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Create and navigate into folder - const folderName = `UnicodeFiles_${Date.now()}`; - await createFolderWithName(page, folderName); - await navigateToFolder(page, folderName); - - // Upload unicode file - const filename = getUniqueTestFilename('日本語ファイル.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Verify file is in folder - const fileCount = await getFileCount(page); - expect(fileCount).toBe(1); - - // Insert into editor - await insertFileIntoEditor(page, filename); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, filename); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Open file manager via TinyMCE to navigate back to root - await openFileManagerViaTinyMCE(page); - await navigateToRoot(page); - - // Verify root has no files (only folder) - const rootFileCount = await getFileCount(page); - expect(rootFileCount).toBe(0); - - // Close file manager - await closeFileManager(page); - - // Cancel TinyMCE dialog if open (from openFileManagerViaTinyMCE) - const cancelBtn = page.locator('.tox-dialog .tox-button:has-text("Cancel")'); - if ((await cancelBtn.count()) > 0) { - await cancelBtn.click(); - await page.waitForTimeout(300); - } - - // Save project - await saveProject(page); - }); - - test('should create nested folders and navigate with breadcrumbs', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Folder - Nested'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Create parent folder - const parentFolder = `Parent_${Date.now()}`; - await createFolderWithName(page, parentFolder); - await navigateToFolder(page, parentFolder); - - // Create child folder - const childFolder = `Child_${Date.now()}`; - await createFolderWithName(page, childFolder); - await navigateToFolder(page, childFolder); - - // Verify breadcrumbs show full path - const breadcrumbs = page.locator('#modalFileManager .media-library-breadcrumbs'); - await expect(breadcrumbs).toContainText(parentFolder); - await expect(breadcrumbs).toContainText(childFolder); - - // Navigate to root using home breadcrumb - await navigateToRoot(page); - - // Verify back at root - const exists = await folderExists(page, parentFolder); - expect(exists).toBe(true); - }); - }); - - test.describe('Rename Operations', () => { - test('should rename file to unicode name', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Rename - To Unicode'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Upload a regular file - await uploadFixtureFile(page, 'test/fixtures/sample-2.jpg'); - - // Get the filename - const files = await getAllFilenames(page); - const originalName = files[0]; - - // Rename to unicode - const newName = `日本語ファイル_${Date.now()}.jpg`; - await renameFile(page, originalName, newName); - - // Verify file was renamed - const updatedFiles = await getAllFilenames(page); - expect(updatedFiles).toContain(newName); - expect(updatedFiles).not.toContain(originalName); - - // Insert renamed file into editor - await insertFileIntoEditor(page, newName); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, newName); - expect(inEditor).toBe(true); - - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } - - // Save project - await saveProject(page); - }); - - test('should rename unicode file to normal name', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Rename - From Unicode'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Upload unicode file - const unicodeName = getUniqueTestFilename('中文文件.jpg'); - await uploadFileWithSpecialName(page, unicodeName); - - // Rename to normal - const newName = `renamed_file_${Date.now()}.jpg`; - await renameFile(page, unicodeName, newName); - - // Verify file was renamed - const updatedFiles = await getAllFilenames(page); - expect(updatedFiles).toContain(newName); - expect(updatedFiles).not.toContain(unicodeName); - - // Insert renamed file into editor - await insertFileIntoEditor(page, newName); - - // Verify in editor - const inEditor = await verifyImageInEditor(page, newName); - expect(inEditor).toBe(true); + await uploadFileWithSpecialName(page, filename1); + await waitForFileInGrid(page, filename1); + await page.waitForTimeout(500); - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } + await uploadFileWithSpecialName(page, filename2); + await waitForFileInGrid(page, filename2); - // Save project - await saveProject(page); + const allFiles = await getAllFilenames(page); + expect(allFiles).toContain(filename1); + expect(allFiles).toContain(filename2); }); + }); - test('should rename file with spaces to file with spaces', async ({ authenticatedPage, createProject }) => { + test.describe('Folder Operations', () => { + test('should create, navigate, and nest folders with unicode files', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Rename - Spaces'); + const projectUuid = await createProject(page, 'Folder Operations'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload file with spaces - const originalName = `file with spaces ${Date.now()}.jpg`; - await uploadFileWithSpecialName(page, originalName); + const ts = Date.now(); - // Rename to different name with spaces - const newName = `new name with spaces ${Date.now()}.jpg`; - await renameFile(page, originalName, newName); + // Create valid alphanumeric folder + const folderName = `TestFolder_${ts}`; + expect(await createFolderWithName(page, folderName)).toBe(true); + expect(await folderExists(page, folderName)).toBe(true); - // Verify rename - const updatedFiles = await getAllFilenames(page); - expect(updatedFiles).toContain(newName); + // Create folder with hyphen/underscore + const folderName2 = `My-Folder_Name_${ts}`; + expect(await createFolderWithName(page, folderName2)).toBe(true); + expect(await folderExists(page, folderName2)).toBe(true); - // Insert renamed file into editor - await insertFileIntoEditor(page, newName); + // Navigate into first folder + await navigateToFolder(page, folderName); + const breadcrumbs = page.locator('#modalFileManager .media-library-breadcrumbs'); + await expect(breadcrumbs).toContainText(folderName); - // Verify in editor - const inEditor = await verifyImageInEditor(page, newName); - expect(inEditor).toBe(true); + // Create nested child folder + const childFolder = `Child_${ts}`; + await createFolderWithName(page, childFolder); + await navigateToFolder(page, childFolder); + await expect(breadcrumbs).toContainText(folderName); + await expect(breadcrumbs).toContainText(childFolder); - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } + // Navigate back to root + await navigateToRoot(page); + expect(await folderExists(page, folderName)).toBe(true); - // Save project - await saveProject(page); + // Verify unicode folder is sanitized or rejected (no crash) + const unicodeFolderResult = await createFolderWithName(page, '日本語フォルダ'); + const folderCount = await getFolderCount(page); + expect(folderCount).toBeGreaterThanOrEqual(0); + void unicodeFolderResult; // may succeed (sanitized) or fail (rejected) }); - test('should preserve extension when renaming', async ({ authenticatedPage, createProject }) => { + test('should upload unicode file inside folder and verify breadcrumb navigation', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Rename - Preserve Ext'); + const projectUuid = await createProject(page, 'Folder - Upload Unicode'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload file - const originalName = getUniqueTestFilename('testfile.jpg'); - await uploadFileWithSpecialName(page, originalName); + const folderName = `UnicodeFiles_${Date.now()}`; + await createFolderWithName(page, folderName); + await navigateToFolder(page, folderName); - // Rename keeping .jpg extension - const newName = `renamed_${Date.now()}.jpg`; - await renameFile(page, originalName, newName); + const filename = getUniqueTestFilename('日本語ファイル.jpg'); + await uploadFileWithSpecialName(page, filename); + expect(await getFileCount(page)).toBe(1); - // Verify extension is preserved - const updatedFiles = await getAllFilenames(page); - expect(updatedFiles).toContain(newName); - expect(newName).toMatch(/\.jpg$/); + await insertFileIntoEditor(page, filename); + expect(await verifyImageInEditor(page, filename)).toBe(true); - // Insert renamed file into editor - await insertFileIntoEditor(page, newName); + await saveTextIdevice(page); - // Verify in editor - const inEditor = await verifyImageInEditor(page, newName); - expect(inEditor).toBe(true); + // Re-open and verify folder structure from root + await openFileManagerViaTinyMCE(page); + await navigateToRoot(page); + expect(await getFileCount(page)).toBe(0); // no files at root + expect(await folderExists(page, folderName)).toBe(true); - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); + await closeFileManager(page); + const cancelBtn = page.locator('.tox-dialog .tox-button:has-text("Cancel")'); + if ((await cancelBtn.count()) > 0) { + await cancelBtn.click(); } - // Save project await saveProject(page); }); + }); - test('should update sidebar after rename', async ({ authenticatedPage, createProject }) => { + test.describe('Rename Operations', () => { + test('should rename files between unicode and normal names', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Rename - Sidebar Update'); + const projectUuid = await createProject(page, 'Rename Operations'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload file - const originalName = getUniqueTestFilename('original.jpg'); - await uploadFileWithSpecialName(page, originalName); + const ts = Date.now(); + + // Upload fixture, rename to unicode + await uploadFixtureFile(page, 'test/fixtures/sample-2.jpg'); + const files = await getAllFilenames(page); + const originalName = files[0]; + const unicodeName = `日本語ファイル_${ts}.jpg`; + await renameFile(page, originalName, unicodeName); - // Rename - const newName = getUniqueTestFilename('新しい名前.jpg'); - await renameFile(page, originalName, newName); + let updatedFiles = await getAllFilenames(page); + expect(updatedFiles).toContain(unicodeName); + expect(updatedFiles).not.toContain(originalName); - // Select the renamed file and verify sidebar - await selectFileByName(page, newName); - const sidebarCorrect = await verifySidebarFilename(page, newName); - expect(sidebarCorrect).toBe(true); + await selectFileByName(page, unicodeName); + expect(await verifySidebarFilename(page, unicodeName)).toBe(true); - // Insert renamed file into editor - await insertFileIntoEditor(page, newName); + // Upload another file (unicode) and rename to normal + await uploadFileWithSpecialName(page, getUniqueTestFilename('中文文件.jpg')); + updatedFiles = await getAllFilenames(page); + const chineseName = updatedFiles.find(f => f.includes('中文'))!; + const normalName = `renamed_file_${ts}.jpg`; + await renameFile(page, chineseName, normalName); - // Verify in editor - const inEditor = await verifyImageInEditor(page, newName); - expect(inEditor).toBe(true); + updatedFiles = await getAllFilenames(page); + expect(updatedFiles).toContain(normalName); - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } + // Insert renamed unicode file and verify + await insertFileIntoEditor(page, unicodeName); + expect(await verifyImageInEditor(page, unicodeName)).toBe(true); - // Save project + await saveTextIdevice(page); await saveProject(page); }); - test('should rename file with multiple dots', async ({ authenticatedPage, createProject }) => { + test('should rename files with spaces, dots, and preserve extension', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Rename - Multiple Dots'); + const projectUuid = await createProject(page, 'Rename - Spaces and Dots'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload file - const originalName = getUniqueTestFilename('test.jpg'); - await uploadFileWithSpecialName(page, originalName); - - // Rename to name with multiple dots - const newName = `file.with.multiple.dots.${Date.now()}.jpg`; - await renameFile(page, originalName, newName); + const ts = Date.now(); - // Verify rename - const updatedFiles = await getAllFilenames(page); - expect(updatedFiles).toContain(newName); + // Upload and rename to name with spaces + const spacesOriginal = `file with spaces ${ts}.jpg`; + await uploadFileWithSpecialName(page, spacesOriginal); + const newSpacesName = `new name with spaces ${ts}.jpg`; + await renameFile(page, spacesOriginal, newSpacesName); + expect(await getAllFilenames(page)).toContain(newSpacesName); - // Insert renamed file into editor - await insertFileIntoEditor(page, newName); + // Upload and rename to multiple dots (verify extension preserved) + const dotsOriginal = getUniqueTestFilename('testfile.jpg'); + await uploadFileWithSpecialName(page, dotsOriginal); + const dotsName = `file.with.multiple.dots.${ts}.jpg`; + await renameFile(page, dotsOriginal, dotsName); - // Verify in editor - const inEditor = await verifyImageInEditor(page, newName); - expect(inEditor).toBe(true); + const finalFiles = await getAllFilenames(page); + expect(finalFiles).toContain(dotsName); + expect(dotsName).toMatch(/\.jpg$/); - // Save iDevice - const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice').first(); - if (await saveIdeviceBtn.isVisible().catch(() => false)) { - await saveIdeviceBtn.click(); - await page.waitForTimeout(500); - } + await insertFileIntoEditor(page, newSpacesName); + expect(await verifyImageInEditor(page, newSpacesName)).toBe(true); - // Save project + await saveTextIdevice(page); await saveProject(page); }); }); test.describe('Search Operations', () => { - test('should search by unicode characters', async ({ authenticatedPage, createProject }) => { + test('should search files by unicode and ASCII characters across folders', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Search - Unicode'); + const projectUuid = await createProject(page, 'Search Operations'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload one unicode file + // Upload files for searching const chineseFile = getUniqueTestFilename('中文文件.jpg'); + const japaneseFile = getUniqueTestFilename('日本語ファイル.jpg'); + const asciiFile = getUniqueTestFilename('TestFile.jpg'); + await uploadFileWithSpecialName(page, chineseFile); - await page.waitForTimeout(500); + await uploadFileWithSpecialName(page, japaneseFile); + await uploadFileWithSpecialName(page, asciiFile); - // Search for Chinese - should find the chinese file + // Search for Chinese - should find the Chinese file const chineseResults = await searchAndGetResults(page, '中文'); expect(chineseResults.length).toBeGreaterThanOrEqual(1); expect(chineseResults.some(f => f.includes('中文'))).toBe(true); - // Clear search and verify all files visible again - await clearSearch(page); - const allFiles = await getAllFilenames(page); - expect(allFiles).toContain(chineseFile); - }); - - test('should search partial unicode match', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Search - Partial Unicode'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Upload file - const filename = getUniqueTestFilename('日本語ファイル.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Search for partial match - const results = await searchAndGetResults(page, '日本'); - expect(results.length).toBeGreaterThanOrEqual(1); - expect(results.some(f => f.includes('日本語'))).toBe(true); - }); - - test('should search case-insensitive for ASCII', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Search - Case Insensitive'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Upload file with mixed case - const filename = getUniqueTestFilename('TestFile.jpg'); - await uploadFileWithSpecialName(page, filename); + // Search for partial Japanese match + const japaneseResults = await searchAndGetResults(page, '日本'); + expect(japaneseResults.length).toBeGreaterThanOrEqual(1); + expect(japaneseResults.some(f => f.includes('日本語'))).toBe(true); - // Search with different case - const results = await searchAndGetResults(page, 'testfile'); - expect(results.length).toBeGreaterThanOrEqual(1); - }); + // Case-insensitive ASCII search + const asciiResults = await searchAndGetResults(page, 'testfile'); + expect(asciiResults.length).toBeGreaterThanOrEqual(1); - test('should search across subfolders', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Search - Subfolders'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); + // Verify clear button works + await clearSearch(page); + const allFiles = await getAllFilenames(page); + expect(allFiles.length).toBeGreaterThanOrEqual(3); - // Create folder and upload file inside + // Create subfolder, upload file inside, search from root + await navigateToRoot(page); const folderName = `SearchTest_${Date.now()}`; await createFolderWithName(page, folderName); await navigateToFolder(page, folderName); - const filename = getUniqueTestFilename('中文文件_in_folder.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Navigate back to root + const folderFile = getUniqueTestFilename('中文文件_in_folder.jpg'); + await uploadFileWithSpecialName(page, folderFile); await navigateToRoot(page); - // Search for file from root - const results = await searchAndGetResults(page, '中文'); - expect(results.length).toBeGreaterThanOrEqual(1); - expect(results.some(f => f.includes('中文'))).toBe(true); - }); - - test('should show search indicator and clear button', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Search - Indicator'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Upload file - const filename = getUniqueTestFilename('searchtest.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Search - await searchFiles(page, 'search'); + const folderResults = await searchAndGetResults(page, '中文'); + expect(folderResults.length).toBeGreaterThanOrEqual(1); // Verify search indicator is visible + await searchFiles(page, 'search'); const searchIndicator = page.locator('#modalFileManager .media-library-search-indicator'); await expect(searchIndicator).toBeVisible(); - // Verify clear button works await clearSearch(page); const searchInput = page.locator('#modalFileManager .media-library-search'); await expect(searchInput).toHaveValue(''); }); }); - test.describe('Preview Operations', () => { - test('should show unicode file in preview after insertion', async ({ authenticatedPage, createProject }) => { + test.describe('Preview and Persistence', () => { + test('should insert unicode file into editor and persist after save and reload', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Preview - Unicode'); + const projectUuid = await createProject(page, 'Preview and Persistence'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload unicode file + // Upload and verify thumbnail const filename = getUniqueTestFilename('中文文件.jpg'); await uploadFileWithSpecialName(page, filename); - // Select and insert + // Select and insert into TinyMCE editor await selectFileByName(page, filename); - const insertBtn = page.locator('#modalFileManager .media-library-insert-btn'); await insertBtn.click(); - - // Wait for file manager to close await page.locator('#modalFileManager').waitFor({ state: 'hidden', timeout: 5000 }); - // Fill alt text + // Fill alt text if dialog appears const altTextField = page .locator('.tox-dialog .tox-form__group') .filter({ has: page.locator('label:text-matches("alternativ", "i")') }) .locator('.tox-textfield'); - if (await altTextField.isVisible().catch(() => false)) { await altTextField.fill('Test unicode image'); } - // Click Save + // Click Save in TinyMCE dialog const saveBtn = page.locator( '.tox-dialog .tox-button[title="Save"], .tox-dialog .tox-button:has-text("Save")', ); @@ -1221,82 +542,22 @@ test.describe('File Manager - Special Characters', () => { await saveBtn.first().click(); } - await page.waitForTimeout(500); - // Verify image is in editor const editorFrame = page.frameLocator('iframe.tox-edit-area__iframe').first(); const img = editorFrame.locator('img').first(); await expect(img).toBeVisible({ timeout: 10000 }); - }); - - test('should preserve unicode filename after save and reload', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Preview - Save Reload'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Upload unicode file - const filename = getUniqueTestFilename('日本語ファイル.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Close file manager - await closeFileManager(page); - - // Close TinyMCE dialog if open - const cancelBtn = page.locator('.tox-dialog .tox-button:has-text("Cancel")'); - if ((await cancelBtn.count()) > 0) { - await cancelBtn.click(); - } - - // Save project - await saveProject(page); - await page.waitForTimeout(500); - - // Reload page - await reloadPage(page); - - // Open file manager again - await openFileManagerViaTinyMCE(page); - - // Verify file still exists with correct name - const files = await getAllFilenames(page); - expect(files).toContain(filename); - }); - test('should insert file with spaces and show in editor', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Preview - Spaces'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); + // Also verify with space-named file in a separate editor context await openFileManagerViaTinyMCE(page); - - // Upload file with spaces - const filename = `file with spaces ${Date.now()}.jpg`; - await uploadFileWithSpecialName(page, filename); - - // Select and insert - await selectFileByName(page, filename); - - const insertBtn = page.locator('#modalFileManager .media-library-insert-btn'); - await insertBtn.click(); - - // Wait for file manager to close + const spacesFile = `file with spaces ${Date.now()}.jpg`; + await uploadFileWithSpecialName(page, spacesFile); + await selectFileByName(page, spacesFile); + await page.locator('#modalFileManager .media-library-insert-btn').click(); await page.locator('#modalFileManager').waitFor({ state: 'hidden', timeout: 5000 }); - // Fill alt text and save - const altTextField = page - .locator('.tox-dialog .tox-form__group') - .filter({ has: page.locator('label:text-matches("alternativ", "i")') }) - .locator('.tox-textfield'); - if (await altTextField.isVisible().catch(() => false)) { await altTextField.fill('Test spaces image'); } - - const saveBtn = page.locator( - '.tox-dialog .tox-button[title="Save"], .tox-dialog .tox-button:has-text("Save")', - ); if ( await saveBtn .first() @@ -1305,19 +566,24 @@ test.describe('File Manager - Special Characters', () => { ) { await saveBtn.first().click(); } + await expect(editorFrame.locator('img').first()).toBeVisible({ timeout: 10000 }); - await page.waitForTimeout(500); + await saveTextIdevice(page); - // Verify image is in editor - const editorFrame = page.frameLocator('iframe.tox-edit-area__iframe').first(); - const img = editorFrame.locator('img').first(); - await expect(img).toBeVisible({ timeout: 10000 }); + // Save project and reload to verify persistence + await saveProject(page); + + await reloadPage(page); + + // Open file manager and verify file still exists + await openFileManagerViaTinyMCE(page); + const allFiles = await getAllFilenames(page); + expect(allFiles).toContain(filename); }); }); test.describe('Download as ELPX Operations', () => { // Note: These tests verify full round-trip: upload → insert → editor → save → download → reopen → verify - // The download button (#navbar-button-download-project) has exe-advanced class. test('should download project with unicode files and verify round-trip', async ({ authenticatedPage, createProject, @@ -1328,18 +594,12 @@ test.describe('File Manager - Special Characters', () => { await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload one unicode file const chineseFile = getUniqueTestFilename('中文文件.jpg'); await uploadFileWithSpecialName(page, chineseFile); - - // Insert into editor await insertFileIntoEditor(page, chineseFile); - // Verify in editor - const inEditorBefore = await verifyImageInEditor(page, chineseFile); - expect(inEditorBefore).toBe(true); + expect(await verifyImageInEditor(page, chineseFile)).toBe(true); - // Save the iDevice const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice'); if ( await saveIdeviceBtn @@ -1348,28 +608,21 @@ test.describe('File Manager - Special Characters', () => { .catch(() => false) ) { await saveIdeviceBtn.first().click(); - await page.waitForTimeout(500); } - // Save project await saveProject(page); - // Download as ELPX const download = await downloadProject(page); const elpxPath = await download.path(); const buffer = Buffer.from(await require('fs').promises.readFile(elpxPath)); - // Verify ZIP contains unicode file - const hasChineseFile = await zipContainsFile(buffer, chineseFile); - expect(hasChineseFile).toBe(true); + expect(await zipContainsFile(buffer, chineseFile)).toBe(true); - // Create new project and open the ELPX to verify round-trip const newProjectUuid = await createProject(page, 'Reopened Unicode'); await gotoWorkarea(page, newProjectUuid); await waitForAppReady(page); await openElpFile(page, elpxPath); - // Verify file appears in file manager after import await openFileManagerViaTinyMCE(page); const filesAfterImport = await getAllFilenames(page); expect(filesAfterImport).toContain(chineseFile); @@ -1385,18 +638,12 @@ test.describe('File Manager - Special Characters', () => { await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload file with spaces const spacesFile = `file with spaces ${Date.now()}.jpg`; await uploadFileWithSpecialName(page, spacesFile); - - // Insert into editor await insertFileIntoEditor(page, spacesFile); - // Verify in editor - const inEditorBefore = await verifyImageInEditor(page, spacesFile); - expect(inEditorBefore).toBe(true); + expect(await verifyImageInEditor(page, spacesFile)).toBe(true); - // Save the iDevice const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice'); if ( await saveIdeviceBtn @@ -1405,28 +652,21 @@ test.describe('File Manager - Special Characters', () => { .catch(() => false) ) { await saveIdeviceBtn.first().click(); - await page.waitForTimeout(500); } - // Save project await saveProject(page); - // Download as ELPX const download = await downloadProject(page); const elpxPath = await download.path(); const buffer = Buffer.from(await require('fs').promises.readFile(elpxPath)); - // Verify ZIP contains file with spaces - const hasSpacesFile = await zipContainsFile(buffer, spacesFile); - expect(hasSpacesFile).toBe(true); + expect(await zipContainsFile(buffer, spacesFile)).toBe(true); - // Create new project and open the ELPX to verify round-trip const newProjectUuid = await createProject(page, 'Reopened Spaces'); await gotoWorkarea(page, newProjectUuid); await waitForAppReady(page); await openElpFile(page, elpxPath); - // Verify file appears in file manager after import await openFileManagerViaTinyMCE(page); const filesAfterImport = await getAllFilenames(page); expect(filesAfterImport).toContain(spacesFile); @@ -1442,22 +682,16 @@ test.describe('File Manager - Special Characters', () => { await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Create folder and upload file inside const folderName = `TestFolder_${Date.now()}`; await createFolderWithName(page, folderName); await navigateToFolder(page, folderName); const filename = getUniqueTestFilename('file_in_folder.jpg'); await uploadFileWithSpecialName(page, filename); - - // Insert into editor from within folder await insertFileIntoEditor(page, filename); - // Verify in editor - const inEditorBefore = await verifyImageInEditor(page, filename); - expect(inEditorBefore).toBe(true); + expect(await verifyImageInEditor(page, filename)).toBe(true); - // Save the iDevice const saveIdeviceBtn = page.locator('#node-content article .idevice_node .btn-save-idevice'); if ( await saveIdeviceBtn @@ -1466,37 +700,28 @@ test.describe('File Manager - Special Characters', () => { .catch(() => false) ) { await saveIdeviceBtn.first().click(); - await page.waitForTimeout(500); } - // Save project await saveProject(page); - // Export as ELPX const download = await downloadProject(page); const elpxPath = await download.path(); const buffer = Buffer.from(await require('fs').promises.readFile(elpxPath)); - // Verify file is in export (folder structure preserved in content/resources/) - const hasFile = await zipContainsFile(buffer, filename); - expect(hasFile).toBe(true); + expect(await zipContainsFile(buffer, filename)).toBe(true); - // Create new project and open the ELPX to verify round-trip const newProjectUuid = await createProject(page, 'Reopened Folder'); await gotoWorkarea(page, newProjectUuid); await waitForAppReady(page); await openElpFile(page, elpxPath); - // Verify file appears in file manager after import (navigate to folder first) await openFileManagerViaTinyMCE(page); - // The folder structure should be preserved - const folderExists = await page.evaluate(name => { + const folderExistsAfterImport = await page.evaluate(name => { const folders = document.querySelectorAll('#modalFileManager .media-library-folder'); return Array.from(folders).some(el => el.getAttribute('data-folder-name') === name); }, folderName); - expect(folderExists).toBe(true); + expect(folderExistsAfterImport).toBe(true); - // Navigate into folder and verify file await navigateToFolder(page, folderName); const filesInFolder = await getAllFilenames(page); expect(filesInFolder).toContain(filename); @@ -1504,136 +729,34 @@ test.describe('File Manager - Special Characters', () => { }); test.describe('Delete Operations', () => { - test('should delete file with unicode name', async ({ authenticatedPage, createProject }) => { + test('should delete files with unicode and space names', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Delete - Unicode'); + const projectUuid = await createProject(page, 'Delete Operations'); await gotoWorkarea(page, projectUuid); await waitForAppReady(page); await openFileManagerViaTinyMCE(page); - // Upload unicode file - const filename = getUniqueTestFilename('中文文件.jpg'); - await uploadFileWithSpecialName(page, filename); + // Upload both file types + const unicodeFile = getUniqueTestFilename('中文文件.jpg'); + const spacesFile = `file with spaces ${Date.now()}.jpg`; + await uploadFileWithSpecialName(page, unicodeFile); + await uploadFileWithSpecialName(page, spacesFile); - // Verify file exists + // Verify both exist let files = await getAllFilenames(page); - expect(files).toContain(filename); - - // Delete file - await deleteFile(page, filename); + expect(files).toContain(unicodeFile); + expect(files).toContain(spacesFile); - // Verify file is gone + // Delete unicode file + await deleteFile(page, unicodeFile); files = await getAllFilenames(page); - expect(files).not.toContain(filename); - }); - - test('should delete file with spaces in name', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Delete - Spaces'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Upload file with spaces - const filename = `file with spaces ${Date.now()}.jpg`; - await uploadFileWithSpecialName(page, filename); - - // Verify file exists - let files = await getAllFilenames(page); - expect(files).toContain(filename); + expect(files).not.toContain(unicodeFile); + expect(files).toContain(spacesFile); - // Delete file - await deleteFile(page, filename); - - // Verify file is gone + // Delete spaces file + await deleteFile(page, spacesFile); files = await getAllFilenames(page); - expect(files).not.toContain(filename); - }); - }); - - test.describe('Persistence', () => { - test('should persist unicode files after page reload', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Persist - Unicode'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Upload one unicode file (simpler test for persistence) - const filename = getUniqueTestFilename('中文文件.jpg'); - await uploadFileWithSpecialName(page, filename); - await page.waitForTimeout(500); - - // Close file manager and dialogs - await closeFileManager(page); - const cancelBtn = page.locator('.tox-dialog .tox-button:has-text("Cancel")'); - if ((await cancelBtn.count()) > 0) { - await cancelBtn.click(); - } - - // Save project - await saveProject(page); - await page.waitForTimeout(500); - - // Reload page - await reloadPage(page); - - // Open file manager - await openFileManagerViaTinyMCE(page); - - // Verify file is still present - const allFiles = await getAllFilenames(page); - expect(allFiles).toContain(filename); - }); - - test('should persist folder structure with unicode files after reload', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const projectUuid = await createProject(page, 'Persist - Folder Unicode'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await openFileManagerViaTinyMCE(page); - - // Create folder and upload unicode file - const folderName = `UnicodeFolder_${Date.now()}`; - await createFolderWithName(page, folderName); - await navigateToFolder(page, folderName); - - const filename = getUniqueTestFilename('日本語ファイル.jpg'); - await uploadFileWithSpecialName(page, filename); - - // Navigate back to root - await navigateToRoot(page); - - // Close file manager and dialogs - await closeFileManager(page); - const cancelBtn = page.locator('.tox-dialog .tox-button:has-text("Cancel")'); - if ((await cancelBtn.count()) > 0) { - await cancelBtn.click(); - } - - // Save project - await saveProject(page); - await page.waitForTimeout(500); - - // Reload page - await reloadPage(page); - - // Open file manager - await openFileManagerViaTinyMCE(page); - - // Verify folder exists - const exists = await folderExists(page, folderName); - expect(exists).toBe(true); - - // Navigate into folder - await navigateToFolder(page, folderName); - - // Verify file is still there - const files = await getAllFilenames(page); - expect(files).toContain(filename); + expect(files).not.toContain(spacesFile); }); }); }); diff --git a/test/e2e/playwright/specs/file-manager.spec.ts b/test/e2e/playwright/specs/file-manager.spec.ts index 3c8c2de08..cb2498348 100644 --- a/test/e2e/playwright/specs/file-manager.spec.ts +++ b/test/e2e/playwright/specs/file-manager.spec.ts @@ -1,5 +1,12 @@ import { test, expect } from '../fixtures/auth.fixture'; -import { waitForAppReady, waitForLoadingScreen, reloadPage, gotoWorkarea } from '../helpers/workarea-helpers'; +import { + waitForAppReady, + waitForLoadingScreen, + reloadPage, + gotoWorkarea, + selectFirstPage, + addTextIdevice, +} from '../helpers/workarea-helpers'; import { WorkareaPage } from '../pages/workarea.page'; import type { Page } from '@playwright/test'; @@ -14,89 +21,14 @@ import type { Page } from '@playwright/test'; * - Duplicate files */ -/** - * Helper to add a text iDevice and enter edit mode - */ -async function addTextIdeviceFromPanel(page: Page): Promise { - // First, select a page in the navigation tree - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - await page.waitForTimeout(500); - - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => {}); - - // Try quick access button first - const quickTextButton = page - .locator('[data-testid="quick-idevice-text"], .quick-idevice-btn[data-idevice="text"]') - .first(); - if ((await quickTextButton.count()) > 0 && (await quickTextButton.isVisible())) { - await quickTextButton.click(); - } else { - // Expand "Information and presentation" category - const infoCategory = page - .locator('#menu_idevices .accordion-item') - .filter({ hasText: /Information|Información/i }) - .locator('.accordion-button'); - - if ((await infoCategory.count()) > 0) { - const isCollapsed = await infoCategory.first().evaluate(el => el.classList.contains('collapsed')); - if (isCollapsed) { - await infoCategory.first().click(); - await page.waitForTimeout(500); - } - } - - const textIdevice = page.locator('.idevice_item[id="text"], [data-testid="idevice-text"]').first(); - await textIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await textIdevice.click(); - } - - await page.locator('#node-content article .idevice_node.text').first().waitFor({ timeout: 15000 }); -} - /** * Helper to open the File Manager modal via TinyMCE image dialog */ async function openFileManager(page: Page): Promise { const existingTinyMce = page.locator('.tox-menubar'); if ((await existingTinyMce.count()) === 0) { - await addTextIdeviceFromPanel(page); + await selectFirstPage(page); + await addTextIdevice(page); } await page.waitForSelector('.tox-menubar', { timeout: 15000 }); diff --git a/test/e2e/playwright/specs/idevices-import-regression.spec.ts b/test/e2e/playwright/specs/idevices-import-regression.spec.ts new file mode 100644 index 000000000..08e28882f --- /dev/null +++ b/test/e2e/playwright/specs/idevices-import-regression.spec.ts @@ -0,0 +1,123 @@ +import { test, expect, skipInStaticMode } from '../fixtures/auth.fixture'; +import * as path from 'path'; +import { waitForAppReady, gotoWorkarea, openElpFile } from '../helpers/workarea-helpers'; + +/** + * E2E Regression Test: Import todos-los-idevices.elp + * + * Verifies that the "todos-los-idevices.elp" fixture (containing 33 unique + * iDevice types across 49 pages) imports correctly and all iDevice types + * render in the workarea without errors. + * + * This test provides broad regression coverage for iDevice rendering after + * import without creating per-iDevice projects. It does NOT replace the + * behavior/interaction tests in the idevices/ directory. + * + * ELP fixture: test/fixtures/todos-los-idevices.elp + * - 49 pages, ~50 iDevice instances, 33 unique iDevice types + * - Types: az-quiz-game, beforeafter, challenge, checklist, classify, + * complete, crossword, discover, dragdrop, flipcards, form, + * geogebra-activity, guess, hidden-image, identify, + * interactive-video, map, mathematicaloperations, mathproblems, + * padlock, periodic-table, progress-report, puzzle, + * quick-questions, quick-questions-multiple-choice, + * quick-questions-video, relate, scrambled-list, + * select-media-files, sort, trivial, trueorfalse, word-search + */ + +const ELP_FIXTURE = path.resolve('test/fixtures/todos-los-idevices.elp'); +const MIN_PAGES = 40; +const MIN_UNIQUE_TYPES = 15; + +test.describe('iDevices Import Regression', () => { + test('should import todos-los-idevices.elp and render all iDevice types', async ({ + authenticatedPage, + createProject, + }, testInfo) => { + skipInStaticMode(test, testInfo, 'Requires server project creation and Yjs sync'); + + const page = authenticatedPage; + + // Need a workarea context to call openElpFile + const projectUuid = await createProject(page, 'iDevices Import Regression'); + await gotoWorkarea(page, projectUuid); + await waitForAppReady(page); + + // Open the ELP file — replaces the current empty project + await openElpFile(page, ELP_FIXTURE, MIN_PAGES); + + // Snapshot all nav-ids before iterating — stable even if the DOM reorders + // after each click (index-based nth(i) is unreliable on a dynamic nav tree) + const navIds = await page.evaluate(() => { + const items = document.querySelectorAll('.nav-element:not([nav-id="root"])'); + return Array.from(items) + .map(el => el.getAttribute('nav-id')) + .filter((id): id is string => id !== null && id !== ''); + }); + expect(navIds.length).toBeGreaterThanOrEqual(MIN_PAGES); + + // Iterate through all nav pages and collect iDevice types + const renderedTypes = new Set(); + let pagesWithIdevices = 0; + + for (const navId of navIds) { + // Navigate using stable attribute selector — unaffected by DOM reordering. + // Click .node-text-span (the text span) rather than .nav-element-text (the + // container that also includes dropdown buttons) for a clean navigation click. + const navText = page.locator(`.nav-element[nav-id="${navId}"] .nav-element-text .node-text-span`).first(); + if ((await navText.count()) === 0) continue; + await navText.click({ force: true }); + + // Wait for page content to load deterministically: + // 1. Wait for idevicesEngine to start loading (#node-content data-ready → "false") + // 2. Wait for idevicesEngine to finish loading (#node-content data-ready → "true") + // hideNodeContainerLoadScreen is called with 500ms delay, so data-ready="true" is + // set after all iDevices are rendered. + await page + .waitForFunction( + () => document.querySelector('#node-content')?.getAttribute('data-ready') !== 'true', + undefined, + { timeout: 1000 }, + ) + .catch(() => {}); // Ignore if loading doesn't start (page already current) + await page + .waitForFunction( + () => document.querySelector('#node-content')?.getAttribute('data-ready') === 'true', + undefined, + { timeout: 10000 }, + ) + .catch(() => {}); // Ignore if data-ready was never set (no load screen shown) + + // Collect iDevice types on this page + const ideviceNodes = page.locator('#node-content .idevice_node'); + const nodeCount = await ideviceNodes.count(); + + if (nodeCount > 0) { + pagesWithIdevices++; + for (let j = 0; j < nodeCount; j++) { + const classes = await ideviceNodes.nth(j).getAttribute('class'); + // Extract the iDevice type class (second class after 'idevice_node') + const match = classes?.match(/idevice_node\s+([a-z_-]+)/); + if (match?.[1] && match[1] !== 'undefined') { + renderedTypes.add(match[1]); + } + } + } + } + + // Verify we found a significant number of unique iDevice types + expect(renderedTypes.size).toBeGreaterThanOrEqual(MIN_UNIQUE_TYPES); + + // Verify at least most pages have iDevices + expect(pagesWithIdevices).toBeGreaterThanOrEqual(MIN_PAGES - 5); + + // Verify specific key iDevice types are present (those with dedicated test files) + const expectedTypes = ['form', 'beforeafter', 'interactive-video', 'relate', 'az-quiz-game']; + for (const expectedType of expectedTypes) { + expect(renderedTypes.has(expectedType)).toBe(true); + } + + // Verify no JavaScript errors occurred during import and rendering + // (checked implicitly via successful rendering of all pages) + }); +}); diff --git a/test/e2e/playwright/specs/idevices/az-quiz-game.spec.ts b/test/e2e/playwright/specs/idevices/az-quiz-game.spec.ts index 41d82f63d..9ab3919fd 100644 --- a/test/e2e/playwright/specs/idevices/az-quiz-game.spec.ts +++ b/test/e2e/playwright/specs/idevices/az-quiz-game.spec.ts @@ -1,5 +1,12 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { waitForAppReady, reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + waitForAppReady, + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; import type { Page, FrameLocator } from '@playwright/test'; @@ -8,158 +15,56 @@ import type { Page, FrameLocator } from '@playwright/test'; * * Tests the A-Z Quiz Game iDevice functionality including: * - Basic operations (add, fill words, save) - * - Editing multiple letter entries with words and definitions - * - Preview rendering with rosco wheel and game elements - * - Game interaction (start game, answer questions) + * - Preview rendering with rosco wheel, canvas, letter indicators, and start button + * - Game interaction (start game, answer questions, track score/errors, skip) + * - Configuration (game duration) * - Persistence after reload */ const TEST_DATA = { - projectTitle: 'AZ Quiz Game E2E Test Project', words: [ { letter: 'A', word: 'Apple', definition: 'A red or green fruit that grows on trees' }, { letter: 'B', word: 'Banana', definition: 'A yellow curved tropical fruit' }, { letter: 'C', word: 'Cherry', definition: 'A small red stone fruit' }, ], - gameDuration: '120', }; /** - * Helper to add an A-Z Quiz Game iDevice by selecting the page and clicking the iDevice + * Helper to wait for all 26 word input blocks to initialize */ -async function addAzQuizGameIdeviceFromPanel(page: Page): Promise { - // First, select a page in the navigation tree - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - // Wait for the page content area to switch from metadata to page editor - await page.waitForTimeout(500); - - // Wait for node-content to show page content (not project metadata) +async function waitForWordInputs(page: Page): Promise { await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => { - // Continue anyway - }); - - // Expand "Games" category in iDevices panel - const gamesCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Games|Juegos/i }), + .waitForFunction(() => document.querySelectorAll('.roscoWordMutimediaEdition').length >= 26, undefined, { + timeout: 10000, }) - .first(); - - if ((await gamesCategory.count()) > 0) { - // Check if category is collapsed (has "off" class) - const isCollapsed = await gamesCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - // Click on the .label to expand - const label = gamesCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - // Wait for the category content to be visible - await page.waitForTimeout(500); - - // Find the A-Z Quiz Game iDevice - const azQuizIdevice = page.locator('.idevice_item[id="az-quiz-game"]').first(); - - // Wait for it to be visible and then click - await azQuizIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await azQuizIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.az-quiz-game').first().waitFor({ timeout: 15000 }); - - // Wait for the word inputs to be dynamically created - await page.waitForTimeout(500); - - // Wait for the Words fieldset to have content - await page - .waitForFunction( - () => { - // Check if the word inputs exist (roscoWordEdition class or input with Word placeholder) - const wordInputs = document.querySelectorAll( - '.roscoWordEdition, #roscoDataWord input[placeholder="Word"], #roscoDataWord .roscoWordMutimediaEdition', - ); - return wordInputs.length >= 26; - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => { - // Continue anyway - }); + .catch(() => {}); } /** - * Helper to fill in words for specific letters - * The iDevice has A-Z inputs, we fill in specific ones + * Helper to fill in words and definitions for specific letters */ async function fillWords( page: Page, words: Array<{ letter: string; word: string; definition: string }>, ): Promise { for (const entry of words) { - // Find the word block for this letter by looking at the letter heading const wordBlocks = page.locator('.roscoWordMutimediaEdition'); const count = await wordBlocks.count(); for (let i = 0; i < count; i++) { const block = wordBlocks.nth(i); - const letterHeading = block.locator('h3.roscoLetterEdition'); - const letterText = await letterHeading.textContent(); + const letterText = await block.locator('h3.roscoLetterEdition').textContent(); if (letterText?.trim().toUpperCase() === entry.letter.toUpperCase()) { - // Fill in the word - use input with placeholder "Word" or class const wordInput = block.locator('input.roscoWordEdition, input[placeholder="Word"]').first(); await wordInput.clear(); await wordInput.fill(entry.word); - // Fill in the definition const definitionInput = block .locator('input.roscoDefinitionEdition, input[placeholder="Definition"]') .first(); await definitionInput.clear(); await definitionInput.fill(entry.definition); - - // Trigger blur to update the letter color indicator await definitionInput.blur(); await page.waitForTimeout(300); break; @@ -169,25 +74,21 @@ async function fillWords( } /** - * Helper to set game duration - * Note: The Options fieldset must be expanded first + * Helper to set game duration via the Options fieldset */ async function setGameDuration(page: Page, duration: string): Promise { - // First, expand the Options fieldset by clicking its header link const optionsHeader = page .locator('fieldset legend a:has-text("Options"), fieldset legend a:has-text("Opciones")') .first(); if ((await optionsHeader.count()) > 0) { await optionsHeader.click(); - await page.waitForTimeout(500); + await page.waitForTimeout(300); } const durationInput = page.locator('#roscoDuration'); if ((await durationInput.count()) > 0) { - // Scroll into view and wait for visibility await durationInput.scrollIntoViewIfNeeded(); await durationInput.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}); - await durationInput.clear(); await durationInput.fill(duration); } @@ -202,7 +103,7 @@ async function closeAlertModals(page: Page): Promise { const okBtn = modal.locator('button:has-text("OK"), button:has-text("Aceptar"), .btn-primary').first(); if ((await okBtn.count()) > 0) { await okBtn.click(); - await page.waitForTimeout(500); + await page.waitForTimeout(300); } } } @@ -211,32 +112,22 @@ async function closeAlertModals(page: Page): Promise { * Helper to save the az-quiz-game iDevice */ async function saveAzQuizGameIdevice(page: Page): Promise { - // Close any alert modals first await closeAlertModals(page); - // Find the iDevice block const block = page.locator('#node-content article .idevice_node.az-quiz-game').last(); - - // Find and click the Save button const saveBtn = block.locator('.btn-save-idevice'); try { await saveBtn.click({ timeout: 5000 }); } catch { - // Close modal and try again await closeAlertModals(page); await saveBtn.click(); } - // Wait for save to complete - check for rosco container appearing - await page.waitForTimeout(500); - - // Try to wait for edition mode to end (mode attribute changes) await page .waitForFunction( () => { const idevice = document.querySelector('#node-content article .idevice_node.az-quiz-game'); - // Either mode is not edition, or rosco container exists return ( (idevice && idevice.getAttribute('mode') !== 'edition') || document.querySelector('#node-content .az-quiz-game .rosco-IDevice') !== null @@ -245,101 +136,49 @@ async function saveAzQuizGameIdevice(page: Page): Promise { undefined, { timeout: 10000 }, ) - .catch(() => { - // Continue anyway - }); - - await page.waitForTimeout(500); + .catch(() => {}); } /** - * Helper to check if rosco elements are visible in preview + * Helper to verify the rosco game container is visible in preview */ async function verifyRoscoInPreview(iframe: FrameLocator): Promise { - // Wait for the rosco container await iframe.locator('.rosco-IDevice').first().waitFor({ state: 'visible', timeout: 10000 }); - - // Verify main container is visible - const mainContainer = iframe.locator('[id^="roscoMainContainer-"]').first(); - await expect(mainContainer).toBeVisible({ timeout: 10000 }); + await expect(iframe.locator('[id^="roscoMainContainer-"]').first()).toBeVisible({ timeout: 10000 }); } test.describe('A-Z Quiz Game iDevice', () => { - test.describe('Basic Operations', () => { - test('should add az-quiz-game iDevice to page', async ({ authenticatedPage, createProject }) => { + test.describe('Workflow', () => { + test('should add iDevice, fill words, and save', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; - // Create a new project - const projectUuid = await createProject(page, 'AZ Quiz Add Test'); + const projectUuid = await createProject(page, 'AZ Quiz Workflow Test'); await gotoWorkarea(page, projectUuid); - - // Wait for app initialization await waitForAppReady(page); - // Add an A-Z Quiz Game iDevice - await addAzQuizGameIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Games|Juegos/i); + await addIdevice(page, 'az-quiz-game'); + await waitForWordInputs(page); - // Verify iDevice was added + // Verify form is visible with 26 word inputs (A–Z) const azQuizIdevice = page.locator('#node-content article .idevice_node.az-quiz-game').first(); await expect(azQuizIdevice).toBeVisible({ timeout: 10000 }); - - // Verify the edition form is visible with word inputs - // The word blocks have class .roscoWordMutimediaEdition, each containing an input const wordBlocks = page.locator('.roscoWordMutimediaEdition'); - const wordBlockCount = await wordBlocks.count(); - expect(wordBlockCount).toBeGreaterThanOrEqual(26); // A-Z = 26 letters - }); - - test('should fill words and definitions for multiple letters', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'AZ Quiz Fill Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add iDevice - await addAzQuizGameIdeviceFromPanel(page); - - // Fill in words for A, B, C - await fillWords(page, TEST_DATA.words); - - // Verify the letter indicators changed color (blue for filled) - // Find the A letter heading and check its background color - const aBlock = page.locator('.roscoWordMutimediaEdition').first(); - const aLetterHeading = aBlock.locator('h3.roscoLetterEdition'); - const bgColor = await aLetterHeading.evaluate(el => getComputedStyle(el).backgroundColor); - - // The color should be blue (#0099cc) for filled letters - expect(bgColor).toContain('rgb'); // Just check it's a color value - }); - - test('should save iDevice and verify content displays correctly', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'AZ Quiz Save Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add iDevice - await addAzQuizGameIdeviceFromPanel(page); + expect(await wordBlocks.count()).toBeGreaterThanOrEqual(26); - // Fill in words (required for save to work - at least one word needed) + // Fill words and verify letter color indicator updates await fillWords(page, TEST_DATA.words); + const aBlock = wordBlocks.first(); + const bgColor = await aBlock + .locator('h3.roscoLetterEdition') + .evaluate(el => getComputedStyle(el).backgroundColor); + expect(bgColor).toContain('rgb'); - // Save the iDevice + // Save and verify view mode displays game UI await saveAzQuizGameIdevice(page); - - // Verify the iDevice is saved - check for the rosco container - // The saved iDevice shows the game UI with Hits/Errors counters and a Start button const roscoContent = page.locator('#node-content .az-quiz-game'); await expect(roscoContent.first()).toBeAttached({ timeout: 10000 }); - - // Verify the game data was saved - check for the Start button or stats area const gameStart = page.locator('#node-content .az-quiz-game a:has-text("Click here to start")'); const statsArea = page.locator('#node-content .az-quiz-game:has-text("Hits")'); await expect(gameStart.or(statsArea).first()).toBeAttached({ timeout: 5000 }); @@ -351,44 +190,19 @@ test.describe('A-Z Quiz Game iDevice', () => { const projectUuid = await createProject(page, 'AZ Quiz Persist Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Games|Juegos/i); + await addIdevice(page, 'az-quiz-game'); + await waitForWordInputs(page); await fillWords(page, TEST_DATA.words); await saveAzQuizGameIdevice(page); - - // Save the project await workarea.save(); - await page.waitForTimeout(500); - // Reload the page await reloadPage(page); + await selectFirstPage(page); - // Navigate to the page - const pageNode = page - .locator('.nav-element-text') - .filter({ hasText: /New page|Nueva página/i }) - .first(); - if ((await pageNode.count()) > 0) { - await pageNode.click({ force: true, timeout: 5000 }); - await page.waitForTimeout(500); - } - - // Wait for the iDevice to be rendered - await page - .waitForFunction( - () => { - const idevice = document.querySelector('#node-content .az-quiz-game'); - return idevice !== null; - }, - undefined, - { timeout: 15000 }, - ) - .catch(() => {}); - - // Verify the iDevice is still there - check for rosco content const roscoContent = page.locator( '#node-content .az-quiz-game .rosco-IDevice, #node-content .az-quiz-game .rosco-DataGame', ); @@ -397,397 +211,138 @@ test.describe('A-Z Quiz Game iDevice', () => { }); test.describe('Preview Panel', () => { - test('should display rosco wheel correctly in preview', async ({ authenticatedPage, createProject }) => { + test('should display rosco wheel, canvas, letters, and start button in preview', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); - // Capture console messages for debugging SW issues - const consoleLogs: string[] = []; - page.on('console', msg => { - if (msg.text().includes('[Preview SW]') || msg.text().includes('Service Worker')) { - consoleLogs.push(`[${msg.type()}] ${msg.text()}`); - } - }); - const projectUuid = await createProject(page, 'AZ Quiz Preview Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Games|Juegos/i); + await addIdevice(page, 'az-quiz-game'); + await waitForWordInputs(page); await fillWords(page, TEST_DATA.words); await saveAzQuizGameIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); + await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Wait for page to load with error logging - try { - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - } catch (error) { - console.log('--- SW Console Logs ---'); - consoleLogs.forEach(log => console.log(log)); - console.log('--- End SW Console Logs ---'); - throw error; - } - - // Verify rosco elements are present + // Verify rosco container and main game container await verifyRoscoInPreview(iframe); - }); - - test('should display canvas with rosco letters in preview', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'AZ Quiz Canvas Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); - await fillWords(page, TEST_DATA.words); - await saveAzQuizGameIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Wait for the rosco to initialize - await page.waitForTimeout(500); - - // Check canvas has proper dimensions (not 0x0) + // Verify canvas has proper dimensions (not 0×0) const canvasInfo = await iframe .locator('[id^="roscoCanvas-"]') .first() .evaluate(el => { const canvas = el as HTMLCanvasElement; - return { - width: canvas.width, - height: canvas.height, - hasContext: !!canvas.getContext('2d'), - }; + return { width: canvas.width, height: canvas.height, hasContext: !!canvas.getContext('2d') }; }); - expect(canvasInfo.width).toBeGreaterThan(0); expect(canvasInfo.height).toBeGreaterThan(0); expect(canvasInfo.hasContext).toBe(true); - }); - test('should display letter indicators with correct colors in preview', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'AZ Quiz Letters Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); - await fillWords(page, TEST_DATA.words); - await saveAzQuizGameIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Wait for initialization - await page.waitForTimeout(500); - - // Check that letter indicators exist + // Verify 26 letter indicators exist and filled letters (A, B, C) are not gray const letterIndicators = iframe.locator('.rosco-Letter'); - const letterCount = await letterIndicators.count(); - expect(letterCount).toBeGreaterThanOrEqual(26); - - // Check that filled letters (A, B, C) are blue (not gray) - // The "A" letter should have the blue color (#5877c6 or similar) - const letterA = iframe.locator('[id^="letterRA-"]').first(); - const letterABgColor = await letterA.evaluate(el => getComputedStyle(el).backgroundColor); - // Blue color for filled letters - expect(letterABgColor).not.toContain('rgb(249, 249, 249)'); // Not gray/black (empty) - }); - - test('should show start game button in preview', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'AZ Quiz Start Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); - await fillWords(page, TEST_DATA.words); - await saveAzQuizGameIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Wait for initialization - await page.waitForTimeout(500); + expect(await letterIndicators.count()).toBeGreaterThanOrEqual(26); + const letterABgColor = await iframe + .locator('[id^="letterRA-"]') + .first() + .evaluate(el => getComputedStyle(el).backgroundColor); + expect(letterABgColor).not.toContain('rgb(249, 249, 249)'); - // Check start game button is visible - const startButton = iframe.locator('[id^="roscoStartGame-"]').first(); - await expect(startButton).toBeVisible({ timeout: 10000 }); + // Verify start button is visible + await expect(iframe.locator('[id^="roscoStartGame-"]').first()).toBeVisible({ timeout: 10000 }); }); }); test.describe('Game Interaction', () => { - test('should start game and show question interface', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'AZ Quiz Game Start Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); - await fillWords(page, TEST_DATA.words); - await saveAzQuizGameIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Wait for initialization - await page.waitForTimeout(500); - - // Click start game button - const startButton = iframe.locator('[id^="roscoStartGame-"]').first(); - await startButton.click(); - - // Wait for game to start - await page.waitForTimeout(500); - - // Verify question div is visible - const questionDiv = iframe.locator('[id^="roscoQuestionDiv-"]').first(); - await expect(questionDiv).toBeVisible({ timeout: 10000 }); - - // Verify reply input is visible - const replyInput = iframe.locator('[id^="roscoEdReply-"]').first(); - await expect(replyInput).toBeVisible({ timeout: 5000 }); - - // Verify submit button is visible - const submitBtn = iframe.locator('[id^="roscoBtnReply-"]').first(); - await expect(submitBtn).toBeVisible({ timeout: 5000 }); - }); - - test('should answer question and update score', async ({ authenticatedPage, createProject }) => { + test('should start game, answer correctly, and track errors', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'AZ Quiz Answer Test'); + const projectUuid = await createProject(page, 'AZ Quiz Game Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Games|Juegos/i); + await addIdevice(page, 'az-quiz-game'); + await waitForWordInputs(page); await fillWords(page, TEST_DATA.words); await saveAzQuizGameIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); + await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Wait for initialization - await page.waitForTimeout(500); + // Start game + await iframe.locator('[id^="roscoStartGame-"]').first().click(); - // Click start game button - const startButton = iframe.locator('[id^="roscoStartGame-"]').first(); - await startButton.click(); - await page.waitForTimeout(500); + // Verify question interface appears + await expect(iframe.locator('[id^="roscoQuestionDiv-"]').first()).toBeVisible({ timeout: 10000 }); + await expect(iframe.locator('[id^="roscoEdReply-"]').first()).toBeVisible({ timeout: 5000 }); + await expect(iframe.locator('[id^="roscoBtnReply-"]').first()).toBeVisible({ timeout: 5000 }); - // Get the initial hits count + // Verify counters start at 0 const hitsCounter = iframe.locator('[id^="roscotPHits-"]').first(); - const initialHits = await hitsCounter.textContent(); - expect(initialHits).toBe('0'); - - // The first question should be for letter A (word: Apple) - // Type the correct answer - const replyInput = iframe.locator('[id^="roscoEdReply-"]').first(); - await replyInput.fill('Apple'); - - // Click submit - const submitBtn = iframe.locator('[id^="roscoBtnReply-"]').first(); - await submitBtn.click(); - - // Wait for answer to be processed - await page.waitForTimeout(500); - - // Verify hits counter increased - const newHits = await hitsCounter.textContent(); - expect(newHits).toBe('1'); - }); - - test('should track errors for wrong answers', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'AZ Quiz Error Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); - await fillWords(page, TEST_DATA.words); - await saveAzQuizGameIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Wait for initialization - await page.waitForTimeout(500); - - // Click start game button - const startButton = iframe.locator('[id^="roscoStartGame-"]').first(); - await startButton.click(); - await page.waitForTimeout(500); - - // Get the initial errors count const errorsCounter = iframe.locator('[id^="roscotPErrors-"]').first(); - const initialErrors = await errorsCounter.textContent(); - expect(initialErrors).toBe('0'); - - // Type a wrong answer - const replyInput = iframe.locator('[id^="roscoEdReply-"]').first(); - await replyInput.fill('WrongAnswer'); - - // Click submit - const submitBtn = iframe.locator('[id^="roscoBtnReply-"]').first(); - await submitBtn.click(); - - // Wait for answer to be processed - await page.waitForTimeout(500); - - // Verify errors counter increased - const newErrors = await errorsCounter.textContent(); - expect(newErrors).toBe('1'); + expect(await hitsCounter.textContent()).toBe('0'); + expect(await errorsCounter.textContent()).toBe('0'); + + // Answer correctly (first question is A = Apple) + await iframe.locator('[id^="roscoEdReply-"]').first().fill('Apple'); + await iframe.locator('[id^="roscoBtnReply-"]').first().click(); + await expect(hitsCounter).toHaveText('1', { timeout: 5000 }); + + // Answer wrong on next question + await iframe.locator('[id^="roscoEdReply-"]').first().fill('WrongAnswer'); + await iframe.locator('[id^="roscoBtnReply-"]').first().click(); + await expect(errorsCounter).toHaveText('1', { timeout: 5000 }); }); - test('should allow skipping questions with Move On button', async ({ authenticatedPage, createProject }) => { + test('should skip questions with Move On button', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); const projectUuid = await createProject(page, 'AZ Quiz Skip Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add iDevice and fill words - await addAzQuizGameIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Games|Juegos/i); + await addIdevice(page, 'az-quiz-game'); + await waitForWordInputs(page); await fillWords(page, TEST_DATA.words); await saveAzQuizGameIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Wait for initialization - await page.waitForTimeout(500); + await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Click start game button - const startButton = iframe.locator('[id^="roscoStartGame-"]').first(); - await startButton.click(); - await page.waitForTimeout(500); + await iframe.locator('[id^="roscoStartGame-"]').first().click(); - // Get the first definition text const definitionText = iframe.locator('[id^="roscoPDefinition-"]').first(); const firstDefinition = await definitionText.textContent(); - // Click Move On button to skip - const moveOnBtn = iframe.locator('[id^="roscoBtnMoveOn-"]').first(); - await moveOnBtn.click(); - - // Wait for next question - await page.waitForTimeout(500); + await iframe.locator('[id^="roscoBtnMoveOn-"]').first().click(); - // Verify definition changed (moved to next question) - const newDefinition = await definitionText.textContent(); - expect(newDefinition).not.toBe(firstDefinition); + await expect(definitionText).not.toHaveText(firstDefinition ?? '', { timeout: 5000 }); }); }); @@ -798,41 +353,25 @@ test.describe('A-Z Quiz Game iDevice', () => { const projectUuid = await createProject(page, 'AZ Quiz Duration Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add iDevice - await addAzQuizGameIdeviceFromPanel(page); - - // Fill words + await selectFirstPage(page); + await expandIdeviceCategory(page, /Games|Juegos/i); + await addIdevice(page, 'az-quiz-game'); + await waitForWordInputs(page); await fillWords(page, TEST_DATA.words); - - // Set a specific duration (60 seconds) await setGameDuration(page, '60'); - - // Save await saveAzQuizGameIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Wait for initialization - await page.waitForTimeout(500); + await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Check the time display shows 1:00 (60 seconds) - const timeDisplay = iframe.locator('[id^="roscoPTime-"]').first(); - const timeText = await timeDisplay.textContent(); - expect(timeText).toContain('1:00'); + // Verify time display shows 1:00 (60 seconds) + await expect(iframe.locator('[id^="roscoPTime-"]').first()).toContainText('1:00', { timeout: 10000 }); }); }); }); diff --git a/test/e2e/playwright/specs/idevices/beforeafter.spec.ts b/test/e2e/playwright/specs/idevices/beforeafter.spec.ts index 1d5558d67..511db802d 100644 --- a/test/e2e/playwright/specs/idevices/beforeafter.spec.ts +++ b/test/e2e/playwright/specs/idevices/beforeafter.spec.ts @@ -1,7 +1,13 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; -import type { Page, FrameLocator } from '@playwright/test'; +import type { FrameLocator } from '@playwright/test'; /** * E2E Tests for BeforeAfter iDevice @@ -18,140 +24,39 @@ const TEST_FIXTURES = { afterImage: 'test/fixtures/sample-3.jpg', }; -/** - * Helper to select a page in the navigation tree (required before adding iDevices) - */ -async function selectPageNode(page: Page): Promise { - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - // Wait for the page content area to switch from metadata to page editor - await page.waitForTimeout(500); - - // Wait for node-content to show page content (not project metadata) - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => { - // Continue anyway - }); -} - -/** - * Helper to add a BeforeAfter iDevice by expanding the category and clicking the iDevice - */ -async function addBeforeAfterIdeviceFromPanel(page: Page): Promise { - // First, select a page in the navigation tree - await selectPageNode(page); - - // Expand "Interactive activities" category in iDevices panel - const interactiveCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Interactive|Interactiv/i }), - }) - .first(); - - if ((await interactiveCategory.count()) > 0) { - // Check if category is collapsed (has "off" class) - const isCollapsed = await interactiveCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - // Click on the .label to expand - const label = interactiveCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - // Wait for the category content to be visible - await page.waitForTimeout(500); - - // Find the BeforeAfter iDevice - const beforeAfterIdevice = page - .locator('.idevice_item[id="beforeafter"], [data-testid="idevice-beforeafter"]') - .first(); - - // Wait for it to be visible and then click - await beforeAfterIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await beforeAfterIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.beforeafter').first().waitFor({ timeout: 15000 }); -} - /** * Helper to upload an image via the file picker input - * Opens the media library modal, uploads the file, selects it, and inserts it */ -async function uploadImageViaFilePicker(page: Page, inputSelector: string, fixturePath: string): Promise { - // Click the file picker button next to the input +async function uploadImageViaFilePicker( + page: import('@playwright/test').Page, + inputSelector: string, + fixturePath: string, +): Promise { const input = page.locator(inputSelector); await input.waitFor({ state: 'visible', timeout: 5000 }); - // Find the associated pick button (next sibling with exe-pick class) const pickButton = page.locator(`${inputSelector} + .exe-pick-any-file, ${inputSelector} + .exe-pick-image`); if ((await pickButton.count()) > 0) { await pickButton.click(); } else { - // Alternative: click directly on the input if it has a click handler await input.click(); } - // Wait for Media Library modal to appear await page.waitForSelector('#modalFileManager[data-open="true"], #modalFileManager.show', { timeout: 10000 }); - // Find the file input in the modal const fileInput = page.locator('#modalFileManager .media-library-upload-input'); await fileInput.setInputFiles(fixturePath); - // Wait for upload to complete and item to appear const mediaItem = page.locator('#modalFileManager .media-library-item').first(); await mediaItem.waitFor({ state: 'visible', timeout: 15000 }); - - // Click on the uploaded item to select it await mediaItem.click(); - await page.waitForTimeout(500); - // Click insert button const insertBtn = page.locator( '#modalFileManager .media-library-insert-btn, #modalFileManager button:has-text("Insert"), #modalFileManager button:has-text("Insertar")', ); await insertBtn.first().click(); - // Wait for modal to close await page.waitForFunction( () => { const modal = document.querySelector('#modalFileManager'); @@ -160,50 +65,42 @@ async function uploadImageViaFilePicker(page: Page, inputSelector: string, fixtu undefined, { timeout: 10000 }, ); - - await page.waitForTimeout(500); } /** * Helper to fill the description field and upload Before/After images for a card */ async function fillCardData( - page: Page, + page: import('@playwright/test').Page, description: string, beforeImagePath: string, afterImagePath: string, ): Promise { - // Fill description (required field) const descInput = page.locator('#bfafEDescription'); await descInput.waitFor({ state: 'visible', timeout: 5000 }); await descInput.clear(); await descInput.fill(description); - // Upload Before image await uploadImageViaFilePicker(page, '#bfafEURLImageBack', beforeImagePath); - - // Upload After image await uploadImageViaFilePicker(page, '#bfafEURLImage', afterImagePath); } /** * Helper to add a new card by clicking the Add button */ -async function addNewCard(page: Page): Promise { +async function addNewCard(page: import('@playwright/test').Page): Promise { const addBtn = page.locator('#bfafEAddC'); await addBtn.click(); - await page.waitForTimeout(500); } /** * Helper to save the BeforeAfter iDevice */ -async function saveBeforeAfterIdevice(page: Page): Promise { +async function saveBeforeAfterIdevice(page: import('@playwright/test').Page): Promise { const block = page.locator('#node-content article .idevice_node.beforeafter').first(); const saveBtn = block.locator('.btn-save-idevice'); await saveBtn.click(); - // Wait for edition mode to end await page.waitForFunction( () => { const idevice = document.querySelector('#node-content article .idevice_node.beforeafter'); @@ -216,23 +113,17 @@ async function saveBeforeAfterIdevice(page: Page): Promise { /** * Helper to verify that the first image rendered correctly in preview - * This is the critical test for the cached image bug */ async function verifyFirstImageRendered(iframe: FrameLocator): Promise { - // Wait for the beforeafter container to be visible const container = iframe.locator('.BFAFP-ContainerBA').first(); await container.waitFor({ state: 'visible', timeout: 15000 }); - // Critical check: container should have opacity > 0 (not stuck at 0 due to cached image bug) const opacity = await container.evaluate(el => { const style = window.getComputedStyle(el); return parseFloat(style.opacity); }); - - // If the bug is present, opacity will be 0 expect(opacity).toBeGreaterThan(0); - // Verify images have src set (relative paths served by Service Worker) const beforeImg = iframe.locator('.BFAFP-ImageBefore').first(); const afterImg = iframe.locator('[id^="bfafpImageAfter-"]').first(); @@ -241,52 +132,37 @@ async function verifyFirstImageRendered(iframe: FrameLocator): Promise { expect(beforeSrc).toBeTruthy(); expect(afterSrc).toBeTruthy(); - - // With SW-based preview, assets are served via relative paths (content/resources/...) - // rather than blob URLs. Both approaches are valid for asset resolution. expect(beforeSrc).toMatch(/^(blob:|content\/resources\/)/); expect(afterSrc).toMatch(/^(blob:|content\/resources\/)/); } test.describe('BeforeAfter iDevice', () => { - test.describe('Basic Operations', () => { - test('should add beforeafter iDevice to page', async ({ authenticatedPage, createProject }) => { + test.describe('Workflow', () => { + test('should add, fill image pairs, save, and persist after reload', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); - // Create a new project - const projectUuid = await createProject(page, 'BeforeAfter Basic Test'); + const projectUuid = await createProject(page, 'BeforeAfter Workflow Test'); await gotoWorkarea(page, projectUuid); - // Add a beforeafter iDevice - await addBeforeAfterIdeviceFromPanel(page); + // Add iDevice using centralized helpers + await selectFirstPage(page); + await expandIdeviceCategory(page, /Interactive|Interactiv/i); + await addIdevice(page, 'beforeafter'); - // Verify iDevice was added and is in edition mode + // Verify iDevice was added const idevice = page.locator('#node-content article .idevice_node.beforeafter').first(); await expect(idevice).toBeVisible({ timeout: 10000 }); - - // Verify edition form elements are visible await expect(page.locator('#bfafEDescription')).toBeVisible({ timeout: 5000 }); await expect(page.locator('#bfafEURLImageBack')).toBeVisible({ timeout: 5000 }); await expect(page.locator('#bfafEURLImage')).toBeVisible({ timeout: 5000 }); - }); - - test('should add multiple image pairs and save', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'BeforeAfter Multiple Pairs Test'); - await gotoWorkarea(page, projectUuid); - - // Add a beforeafter iDevice - await addBeforeAfterIdeviceFromPanel(page); // Fill first card - await fillCardData( - page, - 'First pair: Building renovation', - TEST_FIXTURES.beforeImage, - TEST_FIXTURES.afterImage, - ); + const uniqueDescription = `Persistence Test ${Date.now()}`; + await fillCardData(page, uniqueDescription, TEST_FIXTURES.beforeImage, TEST_FIXTURES.afterImage); // Add second card await addNewCard(page); @@ -297,128 +173,83 @@ test.describe('BeforeAfter iDevice', () => { TEST_FIXTURES.beforeImage, ); - // Verify we have 2 cards (check the card counter) + // Verify 2 cards const cardCounter = page.locator('#bfafENumCards'); await expect(cardCounter).toHaveText('2', { timeout: 5000 }); // Save the iDevice await saveBeforeAfterIdevice(page); - // Verify the iDevice is saved and displayed in view mode const viewModeIdevice = page.locator( '#node-content article .idevice_node.beforeafter .beforeafter-IDevice', ); await expect(viewModeIdevice).toBeVisible({ timeout: 10000 }); - // Save project - await workarea.save(); - }); - }); - - test.describe('Preview Panel', () => { - test('should render first image correctly on preview open', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'BeforeAfter Preview First Image Test'); - await gotoWorkarea(page, projectUuid); - - // Add a beforeafter iDevice - await addBeforeAfterIdeviceFromPanel(page); - - // Fill first card with images - await fillCardData(page, 'Test: First image render', TEST_FIXTURES.beforeImage, TEST_FIXTURES.afterImage); - - // Add second card (to test navigation later) - await addNewCard(page); - await fillCardData(page, 'Test: Second image', TEST_FIXTURES.afterImage, TEST_FIXTURES.beforeImage); - - // Save the iDevice - await saveBeforeAfterIdevice(page); - - // Save project + // Save project and reload to verify persistence await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - - // Wait for page to load in iframe - await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Wait for beforeafter to initialize - await page.waitForTimeout(500); + await reloadPage(page); - // CRITICAL TEST: Verify first image rendered correctly - // This catches the cached image race condition bug - await verifyFirstImageRendered(iframe); + // Navigate to the page + await selectFirstPage(page); - // Verify the number info shows "1/2" - const numberInfo = iframe.locator('.BFAFP-NumberInfo').first(); - await expect(numberInfo).toContainText(/1.*2/, { timeout: 5000 }); + const reloadedIdevice = page.locator('#node-content article .idevice_node.beforeafter').first(); + await expect(reloadedIdevice).toBeVisible({ timeout: 15000 }); + await expect(reloadedIdevice).toContainText(uniqueDescription, { timeout: 10000 }); }); + }); - test('should navigate between images with Next/Previous buttons', async ({ + test.describe('Preview Panel', () => { + test('should render first image correctly and navigate between images', async ({ authenticatedPage, createProject, }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'BeforeAfter Navigation Test'); + const projectUuid = await createProject(page, 'BeforeAfter Preview Test'); await gotoWorkarea(page, projectUuid); - // Add a beforeafter iDevice - await addBeforeAfterIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Interactive|Interactiv/i); + await addIdevice(page, 'beforeafter'); // Fill first card - await fillCardData(page, 'Navigation Test Pair 1', TEST_FIXTURES.beforeImage, TEST_FIXTURES.afterImage); + await fillCardData(page, 'Test: First image render', TEST_FIXTURES.beforeImage, TEST_FIXTURES.afterImage); // Add second card await addNewCard(page); - await fillCardData(page, 'Navigation Test Pair 2', TEST_FIXTURES.afterImage, TEST_FIXTURES.beforeImage); + await fillCardData(page, 'Test: Second image', TEST_FIXTURES.afterImage, TEST_FIXTURES.beforeImage); - // Add third card + // Add third card for navigation test await addNewCard(page); await fillCardData(page, 'Navigation Test Pair 3', TEST_FIXTURES.beforeImage, TEST_FIXTURES.afterImage); - // Save the iDevice await saveBeforeAfterIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); // Open preview panel await page.click('#head-bottom-preview'); const previewPanel = page.locator('#previewsidenav'); await expect(previewPanel).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); + await page.waitForTimeout(500); // BeforeAfter CSS opacity transition - // Wait for beforeafter to initialize - await page.waitForTimeout(500); + // CRITICAL TEST: Verify first image rendered correctly (cached image race condition bug) + await verifyFirstImageRendered(iframe); - // Verify we start at image 1/3 + // Verify starts at 1/3 const numberInfo = iframe.locator('.BFAFP-NumberInfo').first(); await expect(numberInfo).toContainText(/1.*3/, { timeout: 5000 }); - // Click Next button + // Click Next and verify navigation const nextBtn = iframe.locator('[id^="bfafNext-"]').first(); await nextBtn.click(); - await page.waitForTimeout(500); - - // Verify we're now at image 2/3 await expect(numberInfo).toContainText(/2.*3/, { timeout: 5000 }); - // Verify image 2 rendered correctly (opacity > 0) + // Verify opacity of image 2 const container = iframe.locator('.BFAFP-ContainerBA').first(); const opacity = await container.evaluate(el => { const style = window.getComputedStyle(el); @@ -426,15 +257,12 @@ test.describe('BeforeAfter iDevice', () => { }); expect(opacity).toBeGreaterThan(0); - // Click Next again to go to image 3 + // Navigate to image 3 then back await nextBtn.click(); - await page.waitForTimeout(500); await expect(numberInfo).toContainText(/3.*3/, { timeout: 5000 }); - // Click Previous to go back to image 2 const prevBtn = iframe.locator('[id^="bfafPrevious-"]').first(); await prevBtn.click(); - await page.waitForTimeout(500); await expect(numberInfo).toContainText(/2.*3/, { timeout: 5000 }); }); @@ -445,78 +273,29 @@ test.describe('BeforeAfter iDevice', () => { const projectUuid = await createProject(page, 'BeforeAfter Slider Test'); await gotoWorkarea(page, projectUuid); - // Add a beforeafter iDevice - await addBeforeAfterIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Interactive|Interactiv/i); + await addIdevice(page, 'beforeafter'); - // Fill first card await fillCardData(page, 'Slider Test', TEST_FIXTURES.beforeImage, TEST_FIXTURES.afterImage); - - // Save the iDevice await saveBeforeAfterIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); const previewPanel = page.locator('#previewsidenav'); await expect(previewPanel).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Wait for beforeafter to initialize - await page.waitForTimeout(500); - - // Verify slider is present const slider = iframe.locator('.BFAFP-Slider').first(); await expect(slider).toBeVisible({ timeout: 10000 }); - // Verify overlay (the draggable comparison area) is present const overlay = iframe.locator('.BFAFP-Overlay').first(); await expect(overlay).toBeVisible({ timeout: 5000 }); - // Verify the overlay has some width (slider functionality is initialized) const overlayWidth = await overlay.evaluate(el => el.offsetWidth); expect(overlayWidth).toBeGreaterThan(0); }); }); - - test.describe('Persistence', () => { - test('should persist after reload', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'BeforeAfter Persistence Test'); - await gotoWorkarea(page, projectUuid); - - // Add a beforeafter iDevice - await addBeforeAfterIdeviceFromPanel(page); - - const uniqueDescription = `Persistence Test ${Date.now()}`; - await fillCardData(page, uniqueDescription, TEST_FIXTURES.beforeImage, TEST_FIXTURES.afterImage); - - // Save the iDevice - await saveBeforeAfterIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Reload the page - await reloadPage(page); - - // Navigate to the page - await selectPageNode(page); - - // Verify beforeafter iDevice is still there - const idevice = page.locator('#node-content article .idevice_node.beforeafter').first(); - await expect(idevice).toBeVisible({ timeout: 15000 }); - - // Verify it contains our unique description - await expect(idevice).toContainText(uniqueDescription, { timeout: 10000 }); - }); - }); }); diff --git a/test/e2e/playwright/specs/idevices/digcompedu.spec.ts b/test/e2e/playwright/specs/idevices/digcompedu.spec.ts index 32489c4bc..03db9f64b 100644 --- a/test/e2e/playwright/specs/idevices/digcompedu.spec.ts +++ b/test/e2e/playwright/specs/idevices/digcompedu.spec.ts @@ -1,7 +1,12 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; -import type { Page } from '@playwright/test'; /** * E2E Tests for DigCompEdu iDevice @@ -16,98 +21,12 @@ import type { Page } from '@playwright/test'; * - Preview rendering with summary table */ -/** - * Helper to add a DigCompEdu iDevice by selecting the page and clicking the iDevice - */ -async function addDigcompeduIdeviceFromPanel(page: Page): Promise { - // First, select a page in the navigation tree - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - // Wait for the page content area to switch from metadata to page editor - await page.waitForTimeout(500); - - // Wait for node-content to show page content (not project metadata) - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => { - // Continue anyway - }); - - // Expand "Information and presentation" category in iDevices panel - const informationCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Information|Información/i }), - }) - .first(); - - if ((await informationCategory.count()) > 0) { - // Check if category is collapsed (has "off" class) - const isCollapsed = await informationCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - // Click on the .label to expand - const label = informationCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - // Wait for the category content to be visible - await page.waitForTimeout(500); - - // Find the DigCompEdu iDevice - const digcompeduIdevice = page.locator('.idevice_item[id="digcompedu"]').first(); - - // Wait for it to be visible and then click - await digcompeduIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await digcompeduIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.digcompedu').first().waitFor({ timeout: 15000 }); -} - /** * Helper to wait for the DigCompEdu framework data to load */ -async function waitForFrameworkDataLoaded(page: Page): Promise { - // Wait for the editor container to appear +async function waitForFrameworkDataLoaded(page: import('@playwright/test').Page): Promise { await page.locator('.digcompedu-editor').first().waitFor({ timeout: 15000 }); - // Wait for framework data to load (table should have rows) await page.waitForFunction( () => { const tableBody = document.querySelector('#digcompeduTableBody'); @@ -125,7 +44,7 @@ async function waitForFrameworkDataLoaded(page: Page): Promise { /** * Helper to select indicators by clicking checkboxes */ -async function selectIndicators(page: Page, count: number): Promise { +async function selectIndicators(page: import('@playwright/test').Page, count: number): Promise { const checkboxes = await page.locator('#digcompeduTableBody input[type="checkbox"]').all(); const selectedIds: string[] = []; @@ -133,7 +52,6 @@ async function selectIndicators(page: Page, count: number): Promise { const checkbox = checkboxes[i]; const id = await checkbox.getAttribute('value'); await checkbox.click(); - await page.waitForTimeout(100); if (id) selectedIds.push(id); } @@ -143,11 +61,10 @@ async function selectIndicators(page: Page, count: number): Promise { /** * Helper to save the DigCompEdu iDevice */ -async function saveDigcompeduIdevice(page: Page): Promise { +async function saveDigcompeduIdevice(page: import('@playwright/test').Page): Promise { const saveBtn = page.locator('#node-content article .idevice_node.digcompedu .btn-save-idevice'); await saveBtn.click(); - // Wait for edition mode to end await page.waitForFunction( () => { const idevice = document.querySelector('#node-content article .idevice_node.digcompedu'); @@ -160,47 +77,35 @@ async function saveDigcompeduIdevice(page: Page): Promise { test.describe('DigCompEdu iDevice', () => { test.describe('Basic Operations', () => { - test('should add digcompedu iDevice and load framework data', async ({ authenticatedPage, createProject }) => { + test('should add iDevice, load framework data, and select indicators', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - // Create a new project - const projectUuid = await createProject(page, 'DigCompEdu Add Test'); + const projectUuid = await createProject(page, 'DigCompEdu Basic Test'); await gotoWorkarea(page, projectUuid); - // Add a DigCompEdu iDevice - await addDigcompeduIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'digcompedu'); - // Wait for framework data to load await waitForFrameworkDataLoaded(page); - // Verify the editor structure + // Verify structure const tableBody = page.locator('#digcompeduTableBody'); const rowCount = await tableBody.locator('tr').count(); expect(rowCount).toBeGreaterThan(0); - // Verify checkboxes are present const checkboxCount = await tableBody.locator('input[type="checkbox"]').count(); expect(checkboxCount).toBeGreaterThan(0); - // Verify the selection counter shows initial state + // Verify initial counter state const counter = page.locator('#digcompeduSelectionCounter'); await expect(counter).toContainText(/No items selected|Ningún elemento/i, { timeout: 5000 }); - }); - - test('should select indicators and update selection counter', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'DigCompEdu Selection Test'); - await gotoWorkarea(page, projectUuid); - - await addDigcompeduIdeviceFromPanel(page); - await waitForFrameworkDataLoaded(page); - // Select 3 indicators + // Select 3 indicators and verify counter await selectIndicators(page, 3); - - // Verify selection counter updated - const counter = page.locator('#digcompeduSelectionCounter'); await expect(counter).toContainText(/Selected items: 3|Elementos seleccionados: 3/i, { timeout: 5000 }); }); @@ -210,10 +115,12 @@ test.describe('DigCompEdu iDevice', () => { const projectUuid = await createProject(page, 'DigCompEdu Filter Test'); await gotoWorkarea(page, projectUuid); - await addDigcompeduIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'digcompedu'); + await waitForFrameworkDataLoaded(page); - // Get initial row count const initialRowCount = await page.locator('#digcompeduTableBody tr').count(); // Uncheck all levels except A1 @@ -222,95 +129,102 @@ test.describe('DigCompEdu iDevice', () => { const checkbox = page.locator(`#digcompedu-filter-${level.toLowerCase()}`); if (await checkbox.isChecked()) { await checkbox.click(); - await page.waitForTimeout(100); } } - // Wait for table to update await page.waitForTimeout(500); - // Get filtered row count - should be less than initial const filteredRowCount = await page.locator('#digcompeduTableBody tr').count(); expect(filteredRowCount).toBeLessThan(initialRowCount); expect(filteredRowCount).toBeGreaterThan(0); }); - test('should preview summary in modal', async ({ authenticatedPage, createProject }) => { + test('should preview summary in modal and reset selection', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'DigCompEdu Preview Test'); + const projectUuid = await createProject(page, 'DigCompEdu Summary Modal Test'); await gotoWorkarea(page, projectUuid); - await addDigcompeduIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'digcompedu'); + await waitForFrameworkDataLoaded(page); - // Select some indicators await selectIndicators(page, 5); - // Click preview summary button + // Preview summary const previewBtn = page.locator('#digcompeduPreviewSummary'); await previewBtn.click(); - // Wait for modal to appear const modal = page.locator('#digcompeduSummaryModal[aria-hidden="false"]'); await expect(modal).toBeVisible({ timeout: 5000 }); - // Verify summary table is present const summaryTable = page.locator('#digcompeduSummaryTablePreview table'); await expect(summaryTable).toBeVisible({ timeout: 5000 }); - // Verify the summary table has the expected structure (areas header row) const headerCells = summaryTable.locator('thead th'); const headerCount = await headerCells.count(); expect(headerCount).toBeGreaterThan(0); - // Close modal await page.locator('#digcompeduSummaryModalClose').click(); await expect(modal).toBeHidden({ timeout: 5000 }); + + // Reset selection + const counter = page.locator('#digcompeduSelectionCounter'); + await expect(counter).toContainText(/Selected items: 5|Elementos seleccionados: 5/i, { timeout: 5000 }); + + const resetBtn = page.locator('#digcompeduResetSelection'); + await resetBtn.click(); + await expect(counter).toContainText(/No items selected|Ningún elemento/i, { timeout: 5000 }); }); - test('should save iDevice and persist selection', async ({ authenticatedPage, createProject }) => { + test('should save, persist after reload, and filter with search input', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'DigCompEdu Save Test'); + const projectUuid = await createProject(page, 'DigCompEdu Save Persist Test'); await gotoWorkarea(page, projectUuid); - await addDigcompeduIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'digcompedu'); + await waitForFrameworkDataLoaded(page); - // Select some indicators - await selectIndicators(page, 3); + // Test search + const initialRowCount = await page.locator('#digcompeduTableBody tr').count(); + const searchInput = page.locator('#digcompeduSearch'); + await searchInput.fill('comunicación'); + await page.waitForTimeout(500); + + const filteredRowCount = await page.locator('#digcompeduTableBody tr').count(); + expect(filteredRowCount).toBeLessThan(initialRowCount); + expect(filteredRowCount).toBeGreaterThan(0); + + await searchInput.clear(); + await page.waitForTimeout(500); + const restoredRowCount = await page.locator('#digcompeduTableBody tr').count(); + expect(restoredRowCount).toBe(initialRowCount); - // Save the iDevice + // Select indicators and save + await selectIndicators(page, 3); await saveDigcompeduIdevice(page); - // Save the project await workarea.save(); - await page.waitForTimeout(500); - // Reload the page await reloadPage(page); - // Navigate to the page - const pageNode = page - .locator('.nav-element-text') - .filter({ hasText: /New page|Nueva página/i }) - .first(); - if ((await pageNode.count()) > 0) { - await pageNode.click({ force: true, timeout: 5000 }); - await page.waitForTimeout(500); - } + await selectFirstPage(page); - // Verify the iDevice shows saved content (not in edit mode) const idevice = page.locator('#node-content article .idevice_node.digcompedu'); await expect(idevice).toBeVisible({ timeout: 15000 }); - // Verify it's showing export mode content (summary table) const summaryContent = idevice.locator('.digcompeduIdeviceContent'); await expect(summaryContent).toBeVisible({ timeout: 10000 }); - - // Verify the summary shows selected count await expect(summaryContent).toContainText(/Selected indicators|Indicadores seleccionados/i, { timeout: 5000, }); @@ -324,182 +238,76 @@ test.describe('DigCompEdu iDevice', () => { const projectUuid = await createProject(page, 'DigCompEdu Granularity Test'); await gotoWorkarea(page, projectUuid); - await addDigcompeduIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'digcompedu'); + await waitForFrameworkDataLoaded(page); - // Change granularity to "Levels" const levelRadio = page.locator('#digcompedu-granularity-level'); await levelRadio.click(); - await page.waitForTimeout(300); - // Now when we select one checkbox, it should select all indicators in that level const firstCheckbox = page.locator('#digcompeduTableBody input[type="checkbox"]').first(); await firstCheckbox.click(); - await page.waitForTimeout(200); - // The selection counter should show more than 1 if there are multiple indicators per level const counter = page.locator('#digcompeduSelectionCounter'); const counterText = await counter.textContent(); - - // Verify that granularity is working (selection was made) expect(counterText).toMatch(/Selected items|Elementos seleccionados/i); }); }); test.describe('Preview Rendering', () => { - test('should display summary table in preview panel', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'DigCompEdu Preview Panel Test'); - await gotoWorkarea(page, projectUuid); - - await addDigcompeduIdeviceFromPanel(page); - await waitForFrameworkDataLoaded(page); - - // Select some indicators - await selectIndicators(page, 5); - - // Save the iDevice - await saveDigcompeduIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - - // Wait for page to load - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Verify the DigCompEdu content is displayed - const digcompeduContent = iframe.locator('.digcompeduIdeviceContent').first(); - await expect(digcompeduContent).toBeVisible({ timeout: 10000 }); - - // Verify the summary table is present - const summaryTable = iframe.locator('.digcompedu-summary-table').first(); - await expect(summaryTable).toBeVisible({ timeout: 10000 }); - - // Verify the table has colored area headers - const areaHeaders = summaryTable.locator('thead th[class*="area"]'); - const areaCount = await areaHeaders.count(); - expect(areaCount).toBeGreaterThan(0); - }); - - test('should display textual summary when table+summary mode is selected', async ({ + test('should display summary table and textual summary in preview panel', async ({ authenticatedPage, createProject, }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'DigCompEdu Summary Mode Test'); + const projectUuid = await createProject(page, 'DigCompEdu Preview Test'); await gotoWorkarea(page, projectUuid); - await addDigcompeduIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'digcompedu'); + await waitForFrameworkDataLoaded(page); // Select "Table + textual summary" display mode const tableSummaryRadio = page.locator('#digcompeduDisplayTableSummary'); await tableSummaryRadio.click(); - await page.waitForTimeout(200); - // Select some indicators await selectIndicators(page, 5); - - // Save the iDevice await saveDigcompeduIdevice(page); - // Save project await workarea.save(); - await page.waitForTimeout(500); // Open preview panel await page.click('#head-bottom-preview'); const previewPanel = page.locator('#previewsidenav'); await expect(previewPanel).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); - - // Wait for page to load await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - // Verify the DigCompEdu content is displayed const digcompeduContent = iframe.locator('.digcompeduIdeviceContent').first(); await expect(digcompeduContent).toBeVisible({ timeout: 10000 }); - // Verify the textual summary is present (only in table+summary mode) + // Verify summary table with colored area headers + const summaryTable = iframe.locator('.digcompedu-summary-table').first(); + await expect(summaryTable).toBeVisible({ timeout: 10000 }); + + const areaHeaders = summaryTable.locator('thead th[class*="area"]'); + const areaCount = await areaHeaders.count(); + expect(areaCount).toBeGreaterThan(0); + + // Verify textual summary (in table+summary mode) const textSummary = iframe.locator('.digcompedu-text-summary').first(); await expect(textSummary).toBeVisible({ timeout: 10000 }); - // Verify the text summary has content (headings and lists) const summaryHeadings = textSummary.locator('h6'); const headingCount = await summaryHeadings.count(); expect(headingCount).toBeGreaterThan(0); }); }); - - test.describe('Reset and Search', () => { - test('should reset selection when reset button is clicked', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'DigCompEdu Reset Test'); - await gotoWorkarea(page, projectUuid); - - await addDigcompeduIdeviceFromPanel(page); - await waitForFrameworkDataLoaded(page); - - // Select some indicators - await selectIndicators(page, 5); - - // Verify selection was made - const counter = page.locator('#digcompeduSelectionCounter'); - await expect(counter).toContainText(/Selected items: 5|Elementos seleccionados: 5/i, { timeout: 5000 }); - - // Click reset button - const resetBtn = page.locator('#digcompeduResetSelection'); - await resetBtn.click(); - await page.waitForTimeout(300); - - // Verify selection was reset - await expect(counter).toContainText(/No items selected|Ningún elemento/i, { timeout: 5000 }); - }); - - test('should filter indicators using search input', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'DigCompEdu Search Test'); - await gotoWorkarea(page, projectUuid); - - await addDigcompeduIdeviceFromPanel(page); - await waitForFrameworkDataLoaded(page); - - // Get initial row count - const initialRowCount = await page.locator('#digcompeduTableBody tr').count(); - - // Type in search input - const searchInput = page.locator('#digcompeduSearch'); - await searchInput.fill('comunicación'); - await page.waitForTimeout(500); - - // Get filtered row count - should be less than initial - const filteredRowCount = await page.locator('#digcompeduTableBody tr').count(); - expect(filteredRowCount).toBeLessThan(initialRowCount); - expect(filteredRowCount).toBeGreaterThan(0); - - // Clear search and verify all rows return - await searchInput.clear(); - await page.waitForTimeout(500); - - const restoredRowCount = await page.locator('#digcompeduTableBody tr').count(); - expect(restoredRowCount).toBe(initialRowCount); - }); - }); }); diff --git a/test/e2e/playwright/specs/idevices/download-source-file.spec.ts b/test/e2e/playwright/specs/idevices/download-source-file.spec.ts index 0c1ed7120..f364bf157 100644 --- a/test/e2e/playwright/specs/idevices/download-source-file.spec.ts +++ b/test/e2e/playwright/specs/idevices/download-source-file.spec.ts @@ -1,5 +1,11 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; import type { Page, FrameLocator } from '@playwright/test'; @@ -7,136 +13,30 @@ import type { Page, FrameLocator } from '@playwright/test'; * E2E Tests for Download Source File iDevice * * Tests the Download Source File iDevice functionality including: - * - Basic operations (add, configure, save) - * - Button customization (text, colors, font size) - * - Preview rendering with download link - * - Verification that exportSource is enabled by default + * - Basic operations (add, configure button text/color/font, save) + * - Preview rendering with download link, ELPX functionality, and project info table + * - Persistence after reload + * - Edit mode (loading previous values) */ const TEST_DATA = { - projectTitle: 'Download Source File E2E Test Project', - defaultButtonText: 'Download .elp file', customButtonText: 'Get Project File', - defaultBgColor: '#107275', customBgColor: '#ff5500', - defaultTextColor: '#ffffff', - customTextColor: '#000000', }; /** - * Helper to select a page in the navigation tree (required before adding iDevices) - */ -async function selectPageNode(page: Page): Promise { - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - await page.waitForTimeout(500); - - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => {}); -} - -/** - * Helper to add a Download Source File iDevice by expanding the category and clicking the iDevice - */ -async function addDownloadSourceFileIdeviceFromPanel(page: Page): Promise { - await selectPageNode(page); - - // Expand "Information and presentation" category - const infoCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Information|Información/i }), - }) - .first(); - - if ((await infoCategory.count()) > 0) { - const isCollapsed = await infoCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - const label = infoCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - await page.waitForTimeout(500); - - // Find and click the Download Source File iDevice - const downloadIdevice = page.locator('.idevice_item[id="download-source-file"]').first(); - await downloadIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await downloadIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.download-source-file').first().waitFor({ timeout: 15000 }); - - // Wait for the form to be created - await page.waitForTimeout(500); - - // Wait for the TinyMCE editor to be visible - await page - .waitForFunction( - () => { - const editor = document.querySelector('.tox-editor-header'); - return editor !== null; - }, - undefined, - { timeout: 15000 }, - ) - .catch(() => {}); -} - -/** - * Helper to set the button text + * Helper to set the button text via JS events (more reliable cross-browser) */ async function setButtonText(page: Page, text: string): Promise { const buttonTextInput = page.locator('#dpiButtonText'); await buttonTextInput.waitFor({ state: 'visible', timeout: 5000 }); - - // Use JavaScript to set value directly - more reliable across browsers await buttonTextInput.evaluate((input, newText) => { const el = input as HTMLInputElement; el.value = newText; - // Trigger input and change events so the iDevice picks up the change el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }, text); - - // Wait for the change to be processed - await page.waitForTimeout(500); + await page.waitForTimeout(300); } /** @@ -146,18 +46,6 @@ async function setButtonBgColor(page: Page, color: string): Promise { const colorInput = page.locator('#dpiButtonBGcolor'); await colorInput.waitFor({ state: 'visible', timeout: 5000 }); await colorInput.fill(color); - // Wait for input to be processed (Firefox needs more time) - await page.waitForTimeout(300); -} - -/** - * Helper to set button text color - */ -async function setButtonTextColor(page: Page, color: string): Promise { - const colorInput = page.locator('#dpiButtonTextColor'); - await colorInput.waitFor({ state: 'visible', timeout: 5000 }); - await colorInput.fill(color); - // Wait for input to be processed (Firefox needs more time) await page.waitForTimeout(300); } @@ -168,22 +56,17 @@ async function setFontSize(page: Page, option: '1' | '1.1' | '1.2' | '1.3' | '1. const fontSizeSelect = page.locator('#dpiButtonFontSize'); await fontSizeSelect.waitFor({ state: 'visible', timeout: 5000 }); await fontSizeSelect.selectOption(option); - // Wait for selection to be processed (Firefox needs more time) await page.waitForTimeout(300); } /** - * Helper to close any alert modals that might be blocking interactions + * Helper to close any alert modals */ async function closeAlertModals(page: Page): Promise { - // Try multiple times to close any modals for (let i = 0; i < 3; i++) { const modal = page.locator('#modalAlert[data-open="true"], .modal.show'); if ((await modal.count()) > 0) { - // Wait for modal to be fully visible await page.waitForTimeout(300); - - // Try various close button selectors - the modal uses "Close" text const closeBtn = modal .locator( 'button:has-text("Close"), button:has-text("Cerrar"), button:has-text("OK"), button:has-text("Aceptar"), .btn-secondary[data-dismiss="modal"], button.close[data-dismiss="modal"]', @@ -191,13 +74,12 @@ async function closeAlertModals(page: Page): Promise { .first(); if ((await closeBtn.count()) > 0) { await closeBtn.click({ force: true }); - await page.waitForTimeout(500); + await page.waitForTimeout(300); } else { - // Try clicking the X button in the header as fallback const xBtn = modal.locator('.modal-header .close').first(); if ((await xBtn.count()) > 0) { await xBtn.click({ force: true }); - await page.waitForTimeout(500); + await page.waitForTimeout(300); } } } else { @@ -222,10 +104,6 @@ async function saveDownloadSourceFileIdevice(page: Page): Promise { await saveBtn.click(); } - // Wait for save to complete - await page.waitForTimeout(500); - - // Wait for edition mode to end await page .waitForFunction( () => { @@ -236,8 +114,6 @@ async function saveDownloadSourceFileIdevice(page: Page): Promise { { timeout: 10000 }, ) .catch(() => {}); - - await page.waitForTimeout(500); } /** @@ -248,301 +124,146 @@ async function verifyDownloadLinkInPreview( expectedButtonText?: string, expectedBgColor?: string, ): Promise { - // Wait for the download link container const downloadLink = iframe.locator('.exe-download-package-link a').first(); await downloadLink.waitFor({ state: 'visible', timeout: 10000 }); if (expectedButtonText) { - const buttonText = await downloadLink.textContent(); - expect(buttonText?.trim()).toBe(expectedButtonText); + expect((await downloadLink.textContent())?.trim()).toBe(expectedButtonText); } if (expectedBgColor) { - const style = await downloadLink.getAttribute('style'); - expect(style).toContain(expectedBgColor); + expect(await downloadLink.getAttribute('style')).toContain(expectedBgColor); } - // Verify the onclick handler is present (indicates proper export transformation) const onclick = await downloadLink.getAttribute('onclick'); expect(onclick).toContain('downloadElpx'); } test.describe('Download Source File iDevice', () => { - test.describe('Basic Operations', () => { - test('should add download-source-file iDevice to page', async ({ authenticatedPage, createProject }) => { + test.describe('Workflow', () => { + test('should add iDevice, configure button text, color, font size, and save', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'Download Source File Add Test'); + const projectUuid = await createProject(page, 'Download Source File Workflow Test'); await gotoWorkarea(page, projectUuid); - // Add a Download Source File iDevice - await addDownloadSourceFileIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'download-source-file'); - // Verify iDevice was added + // Wait for TinyMCE editor to be ready + await page + .waitForFunction(() => document.querySelector('.tox-editor-header') !== null, undefined, { + timeout: 15000, + }) + .catch(() => {}); + + // Verify form elements are visible const downloadIdevice = page.locator('#node-content article .idevice_node.download-source-file').first(); await expect(downloadIdevice).toBeVisible({ timeout: 10000 }); + await expect(page.locator('#dpiButtonText')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('#dpiButtonBGcolor')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('#dpiButtonTextColor')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('#dpiButtonFontSize')).toBeVisible({ timeout: 5000 }); - // Verify the form elements are visible - const buttonTextInput = page.locator('#dpiButtonText'); - await expect(buttonTextInput).toBeVisible({ timeout: 5000 }); - - const bgColorInput = page.locator('#dpiButtonBGcolor'); - await expect(bgColorInput).toBeVisible({ timeout: 5000 }); - - const textColorInput = page.locator('#dpiButtonTextColor'); - await expect(textColorInput).toBeVisible({ timeout: 5000 }); - - const fontSizeSelect = page.locator('#dpiButtonFontSize'); - await expect(fontSizeSelect).toBeVisible({ timeout: 5000 }); - }); - - test('should save iDevice with default values', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'Download Source File Save Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addDownloadSourceFileIdeviceFromPanel(page); + // Configure button customizations + await setButtonText(page, TEST_DATA.customButtonText); + await setButtonBgColor(page, TEST_DATA.customBgColor); + await setFontSize(page, '1.3'); - // Save with default values + // Save and verify view mode await saveDownloadSourceFileIdevice(page); - // Verify the iDevice is saved and shows the download link const downloadLink = page.locator('#node-content .download-source-file .exe-download-package-link a'); await expect(downloadLink).toBeAttached({ timeout: 10000 }); + expect((await downloadLink.textContent())?.trim()).toBe(TEST_DATA.customButtonText); + expect(await downloadLink.getAttribute('style')).toContain(TEST_DATA.customBgColor); + expect(await downloadLink.getAttribute('style')).toContain('font-size:1.3em'); }); - test('should persist after reload', async ({ authenticatedPage, createProject }) => { + test('should persist custom button text after reload', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); const projectUuid = await createProject(page, 'Download Source File Persist Test'); await gotoWorkarea(page, projectUuid); - // Add iDevice and set custom button text - await addDownloadSourceFileIdeviceFromPanel(page); - await setButtonText(page, TEST_DATA.customButtonText); - await saveDownloadSourceFileIdevice(page); - - // Save the project - await workarea.save(); - await page.waitForTimeout(500); - - // Reload the page - await reloadPage(page); - - // Navigate to the page - const pageNode = page - .locator('.nav-element-text') - .filter({ hasText: /New page|Nueva página/i }) - .first(); - if ((await pageNode.count()) > 0) { - await pageNode.click({ force: true, timeout: 5000 }); - await page.waitForTimeout(500); - } - - // Wait for the iDevice to be rendered + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'download-source-file'); await page - .waitForFunction( - () => { - const idevice = document.querySelector('#node-content .download-source-file'); - return idevice !== null; - }, - undefined, - { timeout: 15000 }, - ) + .waitForFunction(() => document.querySelector('.tox-editor-header') !== null, undefined, { + timeout: 15000, + }) .catch(() => {}); - // Verify the download link has the custom text - const downloadLink = page.locator('#node-content .download-source-file .exe-download-package-link a'); - await expect(downloadLink).toBeAttached({ timeout: 15000 }); - - const buttonText = await downloadLink.textContent(); - expect(buttonText?.trim()).toBe(TEST_DATA.customButtonText); - }); - }); - - test.describe('Customization', () => { - test('should set custom button text', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'Download Source File Custom Text Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addDownloadSourceFileIdeviceFromPanel(page); - - // Set custom button text await setButtonText(page, TEST_DATA.customButtonText); - - // Save the iDevice - await saveDownloadSourceFileIdevice(page); - - // Verify the button text - const downloadLink = page.locator('#node-content .download-source-file .exe-download-package-link a'); - const buttonText = await downloadLink.textContent(); - expect(buttonText?.trim()).toBe(TEST_DATA.customButtonText); - }); - - test('should set custom background color', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'Download Source File Custom Color Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addDownloadSourceFileIdeviceFromPanel(page); - - // Set custom background color - await setButtonBgColor(page, TEST_DATA.customBgColor); - - // Save the iDevice await saveDownloadSourceFileIdevice(page); + await workarea.save(); - // Verify the button has the custom color - const downloadLink = page.locator('#node-content .download-source-file .exe-download-package-link a'); - const style = await downloadLink.getAttribute('style'); - expect(style).toContain(TEST_DATA.customBgColor); - }); - - test('should set custom font size', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'Download Source File Font Size Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addDownloadSourceFileIdeviceFromPanel(page); - - // Set font size to 130% - await setFontSize(page, '1.3'); - - // Save the iDevice - await saveDownloadSourceFileIdevice(page); + await reloadPage(page); + await selectFirstPage(page); - // Verify the button has the custom font size const downloadLink = page.locator('#node-content .download-source-file .exe-download-package-link a'); - const style = await downloadLink.getAttribute('style'); - expect(style).toContain('font-size:1.3em'); + await expect(downloadLink).toBeAttached({ timeout: 15000 }); + expect((await downloadLink.textContent())?.trim()).toBe(TEST_DATA.customButtonText); }); }); test.describe('Preview Panel', () => { - test('should display download link correctly in preview', async ({ authenticatedPage, createProject }) => { + test('should display download link, ELPX functionality, and project info in preview', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); const projectUuid = await createProject(page, 'Download Source File Preview Test'); await gotoWorkarea(page, projectUuid); - // Add iDevice and set custom values - await addDownloadSourceFileIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'download-source-file'); + await page + .waitForFunction(() => document.querySelector('.tox-editor-header') !== null, undefined, { + timeout: 15000, + }) + .catch(() => {}); + await setButtonText(page, TEST_DATA.customButtonText); await setButtonBgColor(page, TEST_DATA.customBgColor); await saveDownloadSourceFileIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); + await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Verify the download link in preview + // Verify download link with custom text and color await verifyDownloadLinkInPreview(iframe, TEST_DATA.customButtonText, TEST_DATA.customBgColor); - }); - test('should have ELPX download functionality in preview via postMessage', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Download Source File Manifest Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addDownloadSourceFileIdeviceFromPanel(page); - await saveDownloadSourceFileIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Check that preview has downloadElpx function available - // With SW-based preview, we use manifest-based download approach + // Verify ELPX download functionality (SW manifest or postMessage approach) const downloadInfo = await iframe.locator('html').evaluate(() => { const win = window as any; const fnSource = win.downloadElpx?.toString() || ''; return { hasDownloadElpx: typeof win.downloadElpx === 'function', - // SW preview uses manifest-based approach (checks for __ELPX_MANIFEST__) hasManifestLogic: fnSource.includes('__ELPX_MANIFEST__'), - // Legacy blob preview uses postMessage approach hasPostMessageLogic: fnSource.includes('postMessage') && fnSource.includes('exe-download-elpx'), }; }); - expect(downloadInfo.hasDownloadElpx).toBe(true); - // Either manifest-based (SW preview) or postMessage-based (legacy) is valid expect(downloadInfo.hasManifestLogic || downloadInfo.hasPostMessageLogic).toBe(true); - // Verify onclick handler is present (indicates proper export with ELPX download support) - const downloadLink = iframe.locator('.exe-download-package-link a').first(); - await downloadLink.waitFor({ state: 'visible', timeout: 10000 }); - const onclick = await downloadLink.getAttribute('onclick'); - expect(onclick).toBeTruthy(); - expect(onclick).toContain('downloadElpx'); - }); - - test('should show project info table in preview', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Download Source File Info Table Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addDownloadSourceFileIdeviceFromPanel(page); - await saveDownloadSourceFileIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Verify the project info table is present + // Verify project info table is present with headers const infoTable = iframe.locator('.exe-download-package-instructions .exe-package-info').first(); await expect(infoTable).toBeVisible({ timeout: 10000 }); - - // Verify table has expected headers (Title, Description, Authorship, License) - const tableHeaders = iframe.locator('.exe-download-package-instructions .exe-package-info th'); - const headerCount = await tableHeaders.count(); + const headerCount = await iframe.locator('.exe-download-package-instructions .exe-package-info th').count(); expect(headerCount).toBeGreaterThanOrEqual(1); }); }); @@ -555,34 +276,29 @@ test.describe('Download Source File iDevice', () => { const projectUuid = await createProject(page, 'Download Source File Edit Test'); await gotoWorkarea(page, projectUuid); - // Add iDevice with custom values - await addDownloadSourceFileIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'download-source-file'); + await page + .waitForFunction(() => document.querySelector('.tox-editor-header') !== null, undefined, { + timeout: 15000, + }) + .catch(() => {}); + await setButtonText(page, TEST_DATA.customButtonText); await setButtonBgColor(page, TEST_DATA.customBgColor); await setFontSize(page, '1.2'); await saveDownloadSourceFileIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Click edit button to enter edit mode + // Enter edit mode const editBtn = page.locator('#node-content .download-source-file button:has-text("Edit")').first(); await editBtn.click(); - await page.waitForTimeout(500); - - // Wait for the form to load await page.locator('#dpiButtonText').waitFor({ state: 'visible', timeout: 10000 }); - // Verify the values are loaded correctly - const buttonTextInput = page.locator('#dpiButtonText'); - const loadedText = await buttonTextInput.inputValue(); - expect(loadedText).toBe(TEST_DATA.customButtonText); - - // Verify the font size option is loaded - const fontSizeSelect = page.locator('#dpiButtonFontSize'); - const selectedOption = await fontSizeSelect.inputValue(); - expect(selectedOption).toBe('1.2'); + // Verify previous values are loaded + expect(await page.locator('#dpiButtonText').inputValue()).toBe(TEST_DATA.customButtonText); + expect(await page.locator('#dpiButtonFontSize').inputValue()).toBe('1.2'); }); }); }); diff --git a/test/e2e/playwright/specs/idevices/external-website.spec.ts b/test/e2e/playwright/specs/idevices/external-website.spec.ts index c834ef7c2..efc26ffd7 100644 --- a/test/e2e/playwright/specs/idevices/external-website.spec.ts +++ b/test/e2e/playwright/specs/idevices/external-website.spec.ts @@ -1,5 +1,11 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; import type { Page, FrameLocator } from '@playwright/test'; @@ -9,119 +15,22 @@ import type { Page, FrameLocator } from '@playwright/test'; * Tests the External Website iDevice functionality including: * - Basic operations (add, set URL, select frame height, save) * - URL validation (empty, invalid, valid) - * - Frame height options (small, medium, large, super-size) - * - Preview rendering with iframe + * - Preview rendering with iframe and correct height + * - Edit mode (loading and updating previous values) * - Persistence after reload */ const TEST_DATA = { - projectTitle: 'External Website E2E Test Project', - // Use a reliable HTTPS URL that allows embedding validUrl: 'https://example.com', - // Another URL for testing changes alternativeUrl: 'https://www.wikipedia.org', - // Frame heights by option value - frameHeights: { - small: 200, - medium: 300, - large: 500, - superSize: 800, - }, }; /** - * Helper to select a page in the navigation tree (required before adding iDevices) + * Helper to wait for the URL input to be ready after addIdevice */ -async function selectPageNode(page: Page): Promise { - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - await page.waitForTimeout(500); - - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => {}); -} - -/** - * Helper to add an External Website iDevice by expanding the category and clicking the iDevice - */ -async function addExternalWebsiteIdeviceFromPanel(page: Page): Promise { - await selectPageNode(page); - - // Expand "Information and presentation" category - const infoCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Information|Información/i }), - }) - .first(); - - if ((await infoCategory.count()) > 0) { - const isCollapsed = await infoCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - const label = infoCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - await page.waitForTimeout(500); - - // Find and click the External Website iDevice - const externalWebsiteIdevice = page.locator('.idevice_item[id="external-website"]').first(); - await externalWebsiteIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await externalWebsiteIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.external-website').first().waitFor({ timeout: 15000 }); - - // Wait for the form to be created - await page.waitForTimeout(500); - - // Wait for the URL input to be visible +async function waitForUrlInput(page: Page): Promise { await page - .waitForFunction( - () => { - const urlInput = document.querySelector('#websiteUrl'); - return urlInput !== null; - }, - undefined, - { timeout: 10000 }, - ) + .waitForFunction(() => document.querySelector('#websiteUrl') !== null, undefined, { timeout: 10000 }) .catch(() => {}); } @@ -141,27 +50,31 @@ async function setWebsiteUrl(page: Page, url: string): Promise { async function setFrameHeight(page: Page, option: 'small' | 'medium' | 'large' | 'super-size'): Promise { const sizeSelector = page.locator('#sizeSelector'); await sizeSelector.waitFor({ state: 'visible', timeout: 5000 }); - - const optionValue = { - small: '1', - medium: '2', - large: '3', - 'super-size': '4', - }[option]; - + const optionValue = { small: '1', medium: '2', large: '3', 'super-size': '4' }[option]; await sizeSelector.selectOption(optionValue); } /** - * Helper to close any alert modals that might be blocking interactions + * Helper to close any alert modals */ async function closeAlertModals(page: Page): Promise { const modal = page.locator('#modalAlert[data-open="true"]'); if ((await modal.count()) > 0) { - const okBtn = modal.locator('button:has-text("OK"), button:has-text("Aceptar"), .btn-primary').first(); + // The modalAlert footer button has class btn-secondary and text "Accept"/"Aceptar" + const okBtn = modal.locator('.modal-footer button').first(); if ((await okBtn.count()) > 0) { await okBtn.click(); - await page.waitForTimeout(500); + // Wait for modal to fully close (not just animation start) + await page + .waitForFunction( + () => { + const m = document.querySelector('#modalAlert'); + return !m || m.getAttribute('data-open') !== 'true'; + }, + undefined, + { timeout: 5000 }, + ) + .catch(() => {}); } } } @@ -182,10 +95,6 @@ async function saveExternalWebsiteIdevice(page: Page): Promise { await saveBtn.click(); } - // Wait for save to complete - await page.waitForTimeout(500); - - // Wait for edition mode to end or iframe to appear await page .waitForFunction( () => { @@ -197,85 +106,92 @@ async function saveExternalWebsiteIdevice(page: Page): Promise { { timeout: 10000 }, ) .catch(() => {}); - - await page.waitForTimeout(500); } /** - * Helper to verify iframe in preview + * Helper to verify the embedded iframe in preview */ async function verifyIframeInPreview( iframe: FrameLocator, expectedUrl: string, expectedHeight?: number, ): Promise { - // Wait for the iframe container await iframe.locator('#iframeWebsiteIdevice').first().waitFor({ state: 'visible', timeout: 10000 }); - // Check the iframe src const embeddedIframe = iframe.locator('#iframeWebsiteIdevice iframe').first(); await expect(embeddedIframe).toBeVisible({ timeout: 10000 }); - - const src = await embeddedIframe.getAttribute('src'); - expect(src).toBe(expectedUrl); + expect(await embeddedIframe.getAttribute('src')).toBe(expectedUrl); if (expectedHeight) { - const height = await embeddedIframe.getAttribute('height'); - expect(height).toBe(String(expectedHeight)); + expect(await embeddedIframe.getAttribute('height')).toBe(String(expectedHeight)); } } test.describe('External Website iDevice', () => { - test.describe('Basic Operations', () => { - test('should add external-website iDevice to page', async ({ authenticatedPage, createProject }) => { + test.describe('Workflow', () => { + test('should add iDevice, set URL with frame heights, save, and verify view mode', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'External Website Add Test'); + const projectUuid = await createProject(page, 'External Website Workflow Test'); await gotoWorkarea(page, projectUuid); - // Add an External Website iDevice - await addExternalWebsiteIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'external-website'); + await waitForUrlInput(page); - // Verify iDevice was added + // Verify form elements and default size (medium = 2) const externalWebsiteIdevice = page.locator('#node-content article .idevice_node.external-website').first(); await expect(externalWebsiteIdevice).toBeVisible({ timeout: 10000 }); + await expect(page.locator('#websiteUrl')).toBeVisible({ timeout: 5000 }); + await expect(page.locator('#sizeSelector')).toBeVisible({ timeout: 5000 }); + expect(await page.locator('#sizeSelector').inputValue()).toBe('2'); - // Verify the form is visible with URL input - const urlInput = page.locator('#websiteUrl'); - await expect(urlInput).toBeVisible({ timeout: 5000 }); - - // Verify the size selector is visible - const sizeSelector = page.locator('#sizeSelector'); - await expect(sizeSelector).toBeVisible({ timeout: 5000 }); - - // Verify the default size is "medium" - const selectedOption = await sizeSelector.inputValue(); - expect(selectedOption).toBe('2'); // medium = 2 - }); - - test('should set URL and save iDevice', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'External Website URL Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addExternalWebsiteIdeviceFromPanel(page); - - // Set URL + // Set URL and verify saved iFrame (medium = default) await setWebsiteUrl(page, TEST_DATA.validUrl); - - // Save the iDevice await saveExternalWebsiteIdevice(page); - - // Verify the iDevice is saved and shows the iframe container const iframeContainer = page.locator('#node-content .external-website #iframeWebsiteIdevice'); await expect(iframeContainer).toBeAttached({ timeout: 10000 }); + expect( + await page.locator('#node-content .external-website #iframeWebsiteIdevice iframe').getAttribute('src'), + ).toBe(TEST_DATA.validUrl); - // Verify the iframe has the correct src - const iframe = page.locator('#node-content .external-website #iframeWebsiteIdevice iframe'); - const src = await iframe.getAttribute('src'); - expect(src).toBe(TEST_DATA.validUrl); + // Edit: set small height (200px), save, verify + const editBtn = page.locator('#node-content .external-website button:has-text("Edit")').first(); + await editBtn.click(); + await waitForUrlInput(page); + await setFrameHeight(page, 'small'); + await saveExternalWebsiteIdevice(page); + expect( + await page + .locator('#node-content .external-website #iframeWebsiteIdevice iframe') + .getAttribute('height'), + ).toBe('200'); + + // Edit: set large height (500px), save, verify + await editBtn.click(); + await waitForUrlInput(page); + await setFrameHeight(page, 'large'); + await saveExternalWebsiteIdevice(page); + expect( + await page + .locator('#node-content .external-website #iframeWebsiteIdevice iframe') + .getAttribute('height'), + ).toBe('500'); + + // Edit: set super-size height (800px), save, verify + await editBtn.click(); + await waitForUrlInput(page); + await setFrameHeight(page, 'super-size'); + await saveExternalWebsiteIdevice(page); + expect( + await page + .locator('#node-content .external-website #iframeWebsiteIdevice iframe') + .getAttribute('height'), + ).toBe('800'); }); test('should persist after reload', async ({ authenticatedPage, createProject }) => { @@ -285,116 +201,22 @@ test.describe('External Website iDevice', () => { const projectUuid = await createProject(page, 'External Website Persist Test'); await gotoWorkarea(page, projectUuid); - // Add iDevice and set URL - await addExternalWebsiteIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'external-website'); + await waitForUrlInput(page); await setWebsiteUrl(page, TEST_DATA.validUrl); await saveExternalWebsiteIdevice(page); - - // Save the project await workarea.save(); - await page.waitForTimeout(500); - // Reload the page await reloadPage(page); + await selectFirstPage(page); - // Navigate to the page - const pageNode = page - .locator('.nav-element-text') - .filter({ hasText: /New page|Nueva página/i }) - .first(); - if ((await pageNode.count()) > 0) { - await pageNode.click({ force: true, timeout: 5000 }); - await page.waitForTimeout(500); - } - - // Wait for the iDevice to be rendered - await page - .waitForFunction( - () => { - const idevice = document.querySelector('#node-content .external-website'); - return idevice !== null; - }, - undefined, - { timeout: 15000 }, - ) - .catch(() => {}); - - // Verify the iDevice is still there with the iframe const iframeContainer = page.locator('#node-content .external-website #iframeWebsiteIdevice'); await expect(iframeContainer).toBeAttached({ timeout: 15000 }); - - // Verify the iframe has the correct src - const iframe = page.locator('#node-content .external-website #iframeWebsiteIdevice iframe'); - const src = await iframe.getAttribute('src'); - expect(src).toBe(TEST_DATA.validUrl); - }); - }); - - test.describe('Frame Height Options', () => { - test('should set small frame height', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'External Website Small Height Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addExternalWebsiteIdeviceFromPanel(page); - - // Set URL and small frame height - await setWebsiteUrl(page, TEST_DATA.validUrl); - await setFrameHeight(page, 'small'); - - // Save the iDevice - await saveExternalWebsiteIdevice(page); - - // Verify the iframe has the correct height (200px for small) - const iframe = page.locator('#node-content .external-website #iframeWebsiteIdevice iframe'); - const height = await iframe.getAttribute('height'); - expect(height).toBe('200'); - }); - - test('should set large frame height', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'External Website Large Height Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addExternalWebsiteIdeviceFromPanel(page); - - // Set URL and large frame height - await setWebsiteUrl(page, TEST_DATA.validUrl); - await setFrameHeight(page, 'large'); - - // Save the iDevice - await saveExternalWebsiteIdevice(page); - - // Verify the iframe has the correct height (500px for large) - const iframe = page.locator('#node-content .external-website #iframeWebsiteIdevice iframe'); - const height = await iframe.getAttribute('height'); - expect(height).toBe('500'); - }); - - test('should set super-size frame height', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'External Website SuperSize Height Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addExternalWebsiteIdeviceFromPanel(page); - - // Set URL and super-size frame height - await setWebsiteUrl(page, TEST_DATA.validUrl); - await setFrameHeight(page, 'super-size'); - - // Save the iDevice - await saveExternalWebsiteIdevice(page); - - // Verify the iframe has the correct height (800px for super-size) - const iframe = page.locator('#node-content .external-website #iframeWebsiteIdevice iframe'); - const height = await iframe.getAttribute('height'); - expect(height).toBe('800'); + expect( + await page.locator('#node-content .external-website #iframeWebsiteIdevice iframe').getAttribute('src'), + ).toBe(TEST_DATA.validUrl); }); }); @@ -405,209 +227,113 @@ test.describe('External Website iDevice', () => { const projectUuid = await createProject(page, 'External Website Empty URL Test'); await gotoWorkarea(page, projectUuid); - // Add iDevice - await addExternalWebsiteIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'external-website'); + await waitForUrlInput(page); - // Don't set URL (leave empty) and try to save const block = page.locator('#node-content article .idevice_node.external-website').last(); const saveBtn = block.locator('.btn-save-idevice'); await saveBtn.click(); - - // Wait for alert modal to appear - await page.waitForTimeout(500); - - // Check if an alert modal appeared - const alertModal = page.locator('#modalAlert[data-open="true"], .modal.show'); - await expect(alertModal).toBeVisible({ timeout: 5000 }); - - // Close the alert + await expect(page.locator('#modalAlert[data-open="true"], .modal.show')).toBeVisible({ timeout: 5000 }); await closeAlertModals(page); }); - test('should show error for invalid URL', async ({ authenticatedPage, createProject }) => { + test('should show error for invalid URL and save successfully with valid HTTPS', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - const projectUuid = await createProject(page, 'External Website Invalid URL Test'); + const projectUuid = await createProject(page, 'External Website URL Validation Test'); await gotoWorkarea(page, projectUuid); - // Add iDevice - await addExternalWebsiteIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'external-website'); + await waitForUrlInput(page); - // Set invalid URL + // Set invalid URL and try to save — should show alert await setWebsiteUrl(page, 'not-a-valid-url'); - - // Try to save const block = page.locator('#node-content article .idevice_node.external-website').last(); const saveBtn = block.locator('.btn-save-idevice'); await saveBtn.click(); - - // Wait for alert modal to appear - await page.waitForTimeout(500); - - // Check if an alert modal appeared - const alertModal = page.locator('#modalAlert[data-open="true"], .modal.show'); - await expect(alertModal).toBeVisible({ timeout: 5000 }); - - // Close the alert + await expect(page.locator('#modalAlert[data-open="true"], .modal.show')).toBeVisible({ timeout: 5000 }); await closeAlertModals(page); - }); - - test('should accept valid HTTPS URL', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'External Website HTTPS URL Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice - await addExternalWebsiteIdeviceFromPanel(page); - // Set valid HTTPS URL + // Set valid HTTPS URL — should save successfully await setWebsiteUrl(page, 'https://www.example.org'); - - // Save the iDevice await saveExternalWebsiteIdevice(page); - - // Verify the iframe was created with correct src const iframe = page.locator('#node-content .external-website #iframeWebsiteIdevice iframe'); await expect(iframe).toBeAttached({ timeout: 10000 }); - - const src = await iframe.getAttribute('src'); - expect(src).toBe('https://www.example.org'); + expect(await iframe.getAttribute('src')).toBe('https://www.example.org'); }); }); test.describe('Preview Panel', () => { - test('should display iframe correctly in preview', async ({ authenticatedPage, createProject }) => { + test('should display iframe with correct URL and height in preview', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); const projectUuid = await createProject(page, 'External Website Preview Test'); await gotoWorkarea(page, projectUuid); - // Add iDevice and set URL - await addExternalWebsiteIdeviceFromPanel(page); - await setWebsiteUrl(page, TEST_DATA.validUrl); - await saveExternalWebsiteIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Verify the iframe container is visible in preview - await verifyIframeInPreview(iframe, TEST_DATA.validUrl); - }); - - test('should display iframe with correct height in preview', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'External Website Preview Height Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice with large frame height - await addExternalWebsiteIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'external-website'); + await waitForUrlInput(page); await setWebsiteUrl(page, TEST_DATA.validUrl); await setFrameHeight(page, 'large'); await saveExternalWebsiteIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); + await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Verify the iframe has correct height (500px for large) + // Verify iframe has correct URL and height (500px for large) await verifyIframeInPreview(iframe, TEST_DATA.validUrl, 500); }); }); test.describe('Edit Mode', () => { - test('should load previous URL when editing', async ({ authenticatedPage, createProject }) => { + test('should load previous URL and update URL when re-editing', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); const projectUuid = await createProject(page, 'External Website Edit Test'); await gotoWorkarea(page, projectUuid); - // Add iDevice with URL - await addExternalWebsiteIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'external-website'); + await waitForUrlInput(page); await setWebsiteUrl(page, TEST_DATA.validUrl); await setFrameHeight(page, 'large'); await saveExternalWebsiteIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Click edit button to enter edit mode + // Edit and verify previous values are loaded const editBtn = page.locator('#node-content .external-website button:has-text("Edit")').first(); await editBtn.click(); - await page.waitForTimeout(500); - - // Wait for the form to load await page.locator('#websiteUrl').waitFor({ state: 'visible', timeout: 10000 }); + expect(await page.locator('#websiteUrl').inputValue()).toBe(TEST_DATA.validUrl); + expect(await page.locator('#sizeSelector').inputValue()).toBe('3'); // large = 3 - // Verify the URL is loaded in the input - const urlInput = page.locator('#websiteUrl'); - const loadedUrl = await urlInput.inputValue(); - expect(loadedUrl).toBe(TEST_DATA.validUrl); - - // Verify the size option is loaded - const sizeSelector = page.locator('#sizeSelector'); - const selectedOption = await sizeSelector.inputValue(); - expect(selectedOption).toBe('3'); // large = 3 - }); - - test('should update URL when re-editing', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'External Website Update Test'); - await gotoWorkarea(page, projectUuid); - - // Add iDevice with initial URL - await addExternalWebsiteIdeviceFromPanel(page); - await setWebsiteUrl(page, TEST_DATA.validUrl); - await saveExternalWebsiteIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Click edit button to enter edit mode - const editBtn = page.locator('#node-content .external-website button:has-text("Edit")').first(); - await editBtn.click(); - await page.waitForTimeout(500); - - // Wait for the form to load - await page.locator('#websiteUrl').waitFor({ state: 'visible', timeout: 10000 }); - - // Update the URL + // Update the URL and re-save await setWebsiteUrl(page, TEST_DATA.alternativeUrl); - - // Save again await saveExternalWebsiteIdevice(page); - - // Verify the new URL is set - const iframe = page.locator('#node-content .external-website #iframeWebsiteIdevice iframe'); - const src = await iframe.getAttribute('src'); - expect(src).toBe(TEST_DATA.alternativeUrl); + expect( + await page.locator('#node-content .external-website #iframeWebsiteIdevice iframe').getAttribute('src'), + ).toBe(TEST_DATA.alternativeUrl); }); }); }); diff --git a/test/e2e/playwright/specs/idevices/form.spec.ts b/test/e2e/playwright/specs/idevices/form.spec.ts index 20358b757..b2337f9cd 100644 --- a/test/e2e/playwright/specs/idevices/form.spec.ts +++ b/test/e2e/playwright/specs/idevices/form.spec.ts @@ -1,5 +1,12 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { waitForAppReady, reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + waitForAppReady, + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; import type { Page, FrameLocator } from '@playwright/test'; @@ -7,10 +14,10 @@ import type { Page, FrameLocator } from '@playwright/test'; * E2E Tests for Form iDevice * * Tests the Form (assessment) iDevice functionality including: - * - Basic operations (add form, add questions) - * - Multiple question types (true/false, selection, dropdown, fill) - * - Preview rendering - * - Form interaction (check answers) + * - Adding form and multiple question types (true/false, selection) + * - Preview rendering with check/answers buttons + * - Form interaction (answering questions) + * - Persistence after reload */ /** @@ -19,7 +26,6 @@ import type { Page, FrameLocator } from '@playwright/test'; async function closeAlertModals(page: Page): Promise { const modal = page.locator('#modalAlert[data-open="true"]'); if ((await modal.count()) > 0) { - // Try to click OK or close button const okBtn = modal.locator('button:has-text("OK"), button:has-text("Aceptar"), .btn-primary').first(); if ((await okBtn.count()) > 0) { await okBtn.click(); @@ -28,96 +34,13 @@ async function closeAlertModals(page: Page): Promise { } } -/** - * Helper to select a page in the navigation tree (required before adding iDevices) - */ -async function selectPageNode(page: Page): Promise { - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - await page.waitForTimeout(500); - - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => {}); -} - -/** - * Helper to add a Form iDevice by expanding the category and clicking the iDevice - */ -async function addFormIdeviceFromPanel(page: Page): Promise { - await selectPageNode(page); - - // Expand "Assessment and tracking" or "Evaluación y seguimiento" category - const assessmentCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Assessment|Evaluación/i }), - }) - .first(); - - if ((await assessmentCategory.count()) > 0) { - const isCollapsed = await assessmentCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - const label = assessmentCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - await page.waitForTimeout(500); - - // Find and click the Form iDevice - const formIdevice = page.locator('.idevice_item[id="form"], [data-testid="idevice-form"]').first(); - await formIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await formIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.form').first().waitFor({ timeout: 15000 }); -} - /** * Helper to open the questions panel */ async function openQuestionsPanel(page: Page): Promise { - // Click on the show questions button const showQuestionsBtn = page.locator('#buttonHideShowQuestionsTop').first(); await showQuestionsBtn.waitFor({ state: 'visible', timeout: 5000 }); - // Check if panel is already visible const panel = page.locator('#questionsContainerTop'); const isVisible = await panel.isVisible(); @@ -133,37 +56,30 @@ async function openQuestionsPanel(page: Page): Promise { async function addTrueFalseQuestion(page: Page, questionText: string, answer: boolean): Promise { await openQuestionsPanel(page); - // Click add True/False button const addBtn = page.locator('#buttonAddTrueFalseQuestionTop'); await addBtn.click(); await page.waitForTimeout(500); - // Wait for TinyMCE to initialize - the textarea container should be visible const textareaContainer = page.locator('#formPreviewTextareaContainer'); await textareaContainer.waitFor({ state: 'visible', timeout: 10000 }); - // Wait for TinyMCE iframe to appear await page.waitForTimeout(500); - // Find the TinyMCE editor iframe within the question container const tinyMceIframe = page.locator('#formPreviewTextareaContainer .tox-edit-area__iframe').first(); if ((await tinyMceIframe.count()) > 0) { - // Type into TinyMCE using keyboard const frame = page.frameLocator('#formPreviewTextareaContainer .tox-edit-area__iframe').first(); const body = frame.locator('body'); await body.click(); await page.waitForTimeout(300); await page.keyboard.type(questionText, { delay: 10 }); } else { - // Fallback: try to find any textarea const textarea = page.locator('#formPreviewTextareaContainer textarea').first(); if ((await textarea.count()) > 0) { await textarea.fill(questionText); } } - // Select True or False answer - look for radio buttons in the form const trueRadio = page .locator('input[type="radio"][value="true"], #formPreviewTrueFalseRadioButtons input[value="true"]') .first(); @@ -177,7 +93,6 @@ async function addTrueFalseQuestion(page: Page, questionText: string, answer: bo await falseRadio.check({ force: true }); } - // Save the question - using input button selector const saveBtn = page .locator( 'input[id$="_buttonSaveQuestion"], input.question-button[value*="Save"], input.question-button[value*="Guardar"]', @@ -186,7 +101,6 @@ async function addTrueFalseQuestion(page: Page, questionText: string, answer: bo if ((await saveBtn.count()) > 0) { await saveBtn.click(); } else { - // Fallback to any save button const altSaveBtn = page.locator('.footer-buttons-container input[type="button"]').first(); if ((await altSaveBtn.count()) > 0) { await altSaveBtn.click(); @@ -194,7 +108,6 @@ async function addTrueFalseQuestion(page: Page, questionText: string, answer: bo } await page.waitForTimeout(500); - // Close any alert modals that might appear await closeAlertModals(page); } @@ -209,23 +122,18 @@ async function addSelectionQuestion( ): Promise { await openQuestionsPanel(page); - // Click add Selection button const addBtn = page.locator('#buttonAddSelectionQuestionTop'); await addBtn.click(); await page.waitForTimeout(500); - // Wait for the question container to appear const textareaContainer = page.locator('#formPreviewTextareaContainer'); await textareaContainer.waitFor({ state: 'visible', timeout: 10000 }); - // Wait for TinyMCE to initialize await page.waitForTimeout(500); - // Find the TinyMCE editor iframe for the question text const tinyMceIframes = page.locator('#formPreviewTextareaContainer .tox-edit-area__iframe'); if ((await tinyMceIframes.count()) > 0) { - // Type question into first TinyMCE using keyboard const frame = page.frameLocator('#formPreviewTextareaContainer .tox-edit-area__iframe').first(); const body = frame.locator('body'); await body.click(); @@ -233,7 +141,6 @@ async function addSelectionQuestion( await page.keyboard.type(questionText, { delay: 10 }); } - // If multiple selection, click the toggle button if (isMultiple) { const toggleBtn = page.locator('#buttonRadioCheckboxToggle'); if ((await toggleBtn.count()) > 0) { @@ -241,11 +148,9 @@ async function addSelectionQuestion( } } - // For the first option, find the TinyMCE for it (it's already created) const optionEditors = page.locator('#formPreviewTextareaContainer .tox-tinymce'); const optionCount = await optionEditors.count(); - // Fill the first option if there's more than one editor (question + option) if (optionCount > 1 && options.length > 0) { const optionFrames = page.locator('#formPreviewTextareaContainer .tox-edit-area__iframe'); if ((await optionFrames.count()) > 1) { @@ -256,7 +161,6 @@ async function addSelectionQuestion( await page.keyboard.type(options[0].text, { delay: 10 }); } - // Mark first option as correct if needed if (options[0].correct) { const firstRadio = page.locator('#option_1').first(); if ((await firstRadio.count()) > 0) { @@ -265,14 +169,11 @@ async function addSelectionQuestion( } } - // Add remaining options for (let i = 1; i < options.length; i++) { - // Click add option button const addOptionBtn = page.locator('#formPreview_buttonAddOption'); await addOptionBtn.click(); await page.waitForTimeout(500); - // Fill the new option (it should be the last TinyMCE) const optionFrames = page.locator('#formPreviewTextareaContainer .tox-edit-area__iframe'); const frameCount = await optionFrames.count(); if (frameCount > i + 1) { @@ -283,7 +184,6 @@ async function addSelectionQuestion( await page.keyboard.type(options[i].text, { delay: 10 }); } - // Mark as correct if needed if (options[i].correct) { const optionRadio = page.locator(`#option_${i + 1}`).first(); if ((await optionRadio.count()) > 0) { @@ -292,7 +192,6 @@ async function addSelectionQuestion( } } - // Save the question - using input button selector const saveBtn = page .locator( 'input[id$="_buttonSaveQuestion"], input.question-button[value*="Save"], input.question-button[value*="Guardar"]', @@ -308,7 +207,6 @@ async function addSelectionQuestion( } await page.waitForTimeout(500); - // Close any alert modals that might appear await closeAlertModals(page); } @@ -316,17 +214,14 @@ async function addSelectionQuestion( * Helper to save the Form iDevice */ async function saveFormIdevice(page: Page): Promise { - // Close any alert modals first await closeAlertModals(page); const block = page.locator('#node-content article .idevice_node.form').first(); const saveBtn = block.locator('.btn-save-idevice'); - // Try to click save, handling potential modals try { await saveBtn.click({ timeout: 5000 }); } catch { - // If blocked by modal, close it and retry await closeAlertModals(page); await saveBtn.click({ timeout: 5000 }); } @@ -345,137 +240,78 @@ async function saveFormIdevice(page: Page): Promise { * Helper to verify form renders in preview */ async function verifyFormRendered(iframe: FrameLocator): Promise { - // Wait for form container const formContainer = iframe.locator('.form-IDevice').first(); await formContainer.waitFor({ state: 'visible', timeout: 15000 }); - // Verify questions container exists const questionsContainer = iframe.locator('[id^="form-questions-"]').first(); await expect(questionsContainer).toBeVisible({ timeout: 10000 }); - // Verify check button exists const checkBtn = iframe.locator('[id^="form-button-check-"]').first(); await expect(checkBtn).toBeVisible({ timeout: 5000 }); } test.describe('Form iDevice', () => { - test.describe('Basic Operations', () => { - test('should add form iDevice to page', async ({ authenticatedPage, createProject }) => { + test.describe('Workflow', () => { + test('should add form, add multiple question types, and save', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'Form Basic Test'); + const projectUuid = await createProject(page, 'Form Workflow Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add a form iDevice - await addFormIdeviceFromPanel(page); + // Add form iDevice using centralized helpers + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'form'); - // Verify iDevice was added and is in edition mode + // Verify iDevice was added const idevice = page.locator('#node-content article .idevice_node.form').first(); await expect(idevice).toBeVisible({ timeout: 10000 }); - - // Verify the add question buttons are available await expect(page.locator('#buttonHideShowQuestionsTop')).toBeVisible({ timeout: 5000 }); - }); - - test('should add a true/false question', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'Form TrueFalse Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - await addFormIdeviceFromPanel(page); - - // Add a true/false question + // Add true/false question await addTrueFalseQuestion(page, 'The sky is blue.', true); - - // Verify question was added (should see it in the form preview) const questionPreview = page.locator('#formPreview .question-container, #formPreview [class*="question"]'); await expect(questionPreview.first()).toBeVisible({ timeout: 5000 }); - // Save the iDevice - await saveFormIdevice(page); - - await workarea.save(); - }); - - test('should add a selection question with multiple options', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Form Selection Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - await addFormIdeviceFromPanel(page); - - // Add a selection question + // Add selection question await addSelectionQuestion(page, 'What is the capital of France?', [ { text: 'London', correct: false }, { text: 'Paris', correct: true }, { text: 'Berlin', correct: false }, ]); - // Save the iDevice - await saveFormIdevice(page); - - await workarea.save(); - }); - - test('should add multiple questions of different types', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Form Multiple Questions Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - await addFormIdeviceFromPanel(page); - - // Add first question (true/false) + // Add another true/false question await addTrueFalseQuestion(page, 'Water boils at 100°C.', true); - // Add second question (selection) - await addSelectionQuestion(page, 'Which is a primary color?', [ - { text: 'Red', correct: true }, - { text: 'Green', correct: false }, - { text: 'Orange', correct: false }, - ]); - - // Add third question (true/false) - await addTrueFalseQuestion(page, 'The Earth is flat.', false); - - // Verify we have questions added (look for list items in form preview) + // Verify multiple questions exist const questions = page.locator('#formPreview > li, #formPreview .FormView_question, .FormView_question'); const count = await questions.count(); expect(count).toBeGreaterThanOrEqual(1); - // Save the iDevice + // Save await saveFormIdevice(page); - await workarea.save(); }); }); test.describe('Preview Panel', () => { - test('should render form correctly in preview', async ({ authenticatedPage, createProject }) => { + test('should render form with questions and check/answers buttons in preview', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); const projectUuid = await createProject(page, 'Form Preview Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await addFormIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'form'); - // Add questions await addTrueFalseQuestion(page, 'Preview test question: True or False?', true); await addSelectionQuestion(page, 'Preview test: Select the correct answer', [ { text: 'Option A', correct: false }, @@ -484,63 +320,27 @@ test.describe('Form iDevice', () => { await saveFormIdevice(page); await workarea.save(); - await page.waitForTimeout(500); // Open preview panel await page.click('#head-bottom-preview'); const previewPanel = page.locator('#previewsidenav'); await expect(previewPanel).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - await page.waitForTimeout(500); - - // Verify form renders correctly + // Verify form renders correctly with questions await verifyFormRendered(iframe); - - // Verify questions are displayed const questions = iframe.locator('.FRMP-Question, [class*="question"]'); const count = await questions.count(); expect(count).toBeGreaterThanOrEqual(1); - }); - - test('should have check and reset buttons in preview', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'Form Buttons Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - await addFormIdeviceFromPanel(page); - - await addTrueFalseQuestion(page, 'Test question for buttons', true); - - await saveFormIdevice(page); - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - - await page.waitForTimeout(500); - - // Verify check button exists + // Verify check button is present const checkBtn = iframe.locator('[id^="form-button-check-"]').first(); await expect(checkBtn).toBeVisible({ timeout: 10000 }); - // Verify show answers button exists (if enabled) + // Verify show answers button exists in DOM (may or may not be visible) const showAnswersBtn = iframe.locator('[id^="form-button-show-answers-"]').first(); - // This button may or may not be visible depending on settings - // Just check it exists in the DOM const showAnswersCount = await showAnswersBtn.count(); expect(showAnswersCount).toBeGreaterThanOrEqual(0); }); @@ -553,16 +353,17 @@ test.describe('Form iDevice', () => { const projectUuid = await createProject(page, 'Form TF Interaction Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await addFormIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'form'); await addTrueFalseQuestion(page, 'The sun rises in the east.', true); await saveFormIdevice(page); await workarea.save(); - await page.waitForTimeout(500); + await page.waitForTimeout(500); // let save propagate before preview // Open preview await page.click('#head-bottom-preview'); @@ -571,8 +372,7 @@ test.describe('Form iDevice', () => { const iframe = page.frameLocator('#preview-iframe'); await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - - await page.waitForTimeout(500); + await page.waitForTimeout(500); // form JS initialization // Find and click on "True" radio button const trueRadio = iframe.locator('input[type="radio"][value="true"], label:has-text("True") input'); @@ -599,22 +399,21 @@ test.describe('Form iDevice', () => { const projectUuid = await createProject(page, 'Form Persistence Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - await addFormIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'form'); const uniqueQuestion = `Persistence test question ${Date.now()}`; await addTrueFalseQuestion(page, uniqueQuestion, true); await saveFormIdevice(page); await workarea.save(); - await page.waitForTimeout(500); // Reload await reloadPage(page); - - await selectPageNode(page); + await selectFirstPage(page); // Verify iDevice is still there const idevice = page.locator('#node-content article .idevice_node.form').first(); diff --git a/test/e2e/playwright/specs/idevices/image-gallery.spec.ts b/test/e2e/playwright/specs/idevices/image-gallery.spec.ts index a1e6f7bad..a5500eb80 100644 --- a/test/e2e/playwright/specs/idevices/image-gallery.spec.ts +++ b/test/e2e/playwright/specs/idevices/image-gallery.spec.ts @@ -7,90 +7,40 @@ import { waitForAppReady, reloadPage, gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, } from '../../helpers/workarea-helpers'; /** * E2E Tests for Image Gallery iDevice * * Tests the Image Gallery iDevice functionality including: - * - Basic operations (add to blank document) - * - Image upload via file input + * - Basic operations (add to blank document, upload single image) * - Multiple image support - * - Preview panel display + * - Image controls (remove, modify) + * - Preview panel display with lightbox + * - Image persistence after reload + * - Folder path support for images */ /** - * Helper to add an image-gallery iDevice by selecting the page and clicking the iDevice + * Helper to save the image-gallery iDevice and wait for edition mode to end */ -async function addImageGalleryFromPanel(page: Page): Promise { - // First, select a page in the navigation tree - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } +async function saveGalleryIdevice(page: Page): Promise { + const block = page.locator('#node-content article .idevice_node.image-gallery').first(); + const saveBtn = block.locator('.btn-save-idevice'); + if ((await saveBtn.count()) > 0) { + await saveBtn.click(); } - - // Wait for the page content area to switch from metadata to page editor - await page.waitForTimeout(500); - - // Wait for node-content to show page content - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => { - // Continue anyway - }); - - // Find and click the "Information and presentation" category heading - const categoryHeading = page.locator('#menu_idevices_content h3').filter({ - hasText: /Information|Información/i, - }); - - if ((await categoryHeading.count()) > 0) { - await categoryHeading.first().click(); - await page.waitForTimeout(500); - } - - // Now find the image-gallery iDevice in the expanded category - const imageGalleryIdevice = page - .locator('.idevice_item[id="image-gallery"], [data-testid="idevice-image-gallery"]') - .first(); - - // Wait for it to be visible after expanding category - await imageGalleryIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await imageGalleryIdevice.click(); - - // Wait for iDevice to appear in content area (in edition mode) - await page.locator('#node-content article .idevice_node.image-gallery').first().waitFor({ timeout: 15000 }); + await page.waitForFunction( + () => { + const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); + return idevice && idevice.getAttribute('mode') !== 'edition'; + }, + undefined, + { timeout: 15000 }, + ); } /** @@ -124,109 +74,68 @@ async function uploadImagesToGallery(page: Page, fixturePaths: string[]): Promis test.describe('Image Gallery iDevice', () => { test.describe('Basic Operations', () => { - test('should add image-gallery iDevice to blank document', async ({ authenticatedPage, createProject }) => { + test('should add iDevice, upload single image, and display in gallery', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); - // Create a new project const projectUuid = await createProject(page, 'Image Gallery Basic Test'); await gotoWorkarea(page, projectUuid); - // Add an image-gallery iDevice using the panel - await addImageGalleryFromPanel(page); + // Add image-gallery iDevice + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); // Verify iDevice was added and is in edition mode const galleryIdevice = page.locator('#node-content article .idevice_node.image-gallery').first(); await expect(galleryIdevice).toBeVisible({ timeout: 10000 }); - - // Verify the gallery form elements are visible await expect(page.locator('#addImageButton')).toBeVisible({ timeout: 5000 }); - - // Verify the gallery form exists (imagesContainer may be empty initially) - const imagesContainer = page.locator('#imagesContainer'); - await expect(imagesContainer).toBeAttached({ timeout: 5000 }); - - // Verify the "no images" message is shown initially - const noImagesText = page.locator('#textMsxHide'); - await expect(noImagesText).toBeVisible(); - }); - }); - - test.describe('Image Upload', () => { - test('should upload single image and display in gallery', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Image Gallery Upload Test'); - await gotoWorkarea(page, projectUuid); - - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await expect(page.locator('#imagesContainer')).toBeAttached({ timeout: 5000 }); + await expect(page.locator('#textMsxHide')).toBeVisible(); // Upload a single image await uploadImagesToGallery(page, ['test/fixtures/sample-3.jpg']); // Verify the "no images" message is hidden - const noImagesText = page.locator('#textMsxHide'); - await expect(noImagesText).toBeHidden(); + await expect(page.locator('#textMsxHide')).toBeHidden(); - // Verify an image container was created + // Verify an image container was created with valid origin attribute const imageContainer = page.locator('.imgSelectContainer').first(); await expect(imageContainer).toBeVisible({ timeout: 10000 }); - - // Verify the image is displayed const galleryImage = imageContainer.locator('img.image'); await expect(galleryImage).toBeVisible(); - - // Verify image has origin attribute (the full size image path) - const originAttr = await galleryImage.getAttribute('origin'); - expect(originAttr).toBeTruthy(); - console.log('Image origin:', originAttr); + expect(await galleryImage.getAttribute('origin')).toBeTruthy(); // Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - const saveBtn = block.locator('.btn-save-idevice'); - if ((await saveBtn.count()) > 0) { - await saveBtn.click(); - } + await saveGalleryIdevice(page); - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); - - // Verify the gallery is rendered in view mode + // Verify gallery is rendered in view mode with loaded image const viewModeGallery = page.locator( '#node-content article .idevice_node.image-gallery .imageGallery-IDevice', ); await expect(viewModeGallery).toBeVisible({ timeout: 10000 }); - - // Verify images are displayed in view mode const viewModeImages = viewModeGallery.locator('img'); await expect(viewModeImages.first()).toBeVisible({ timeout: 5000 }); - - // Wait for image to load and verify it loaded correctly - await page.waitForTimeout(500); const naturalWidth = await viewModeImages.first().evaluate((el: HTMLImageElement) => el.naturalWidth); - console.log('View mode image naturalWidth:', naturalWidth); expect(naturalWidth).toBeGreaterThan(0); - // Save project await workarea.save(); }); + }); + test.describe('Image Upload', () => { test('should upload multiple images and display in gallery', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; const projectUuid = await createProject(page, 'Image Gallery Multiple Test'); await gotoWorkarea(page, projectUuid); - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); // Upload multiple images await uploadImagesToGallery(page, ['test/fixtures/sample-2.jpg', 'test/fixtures/sample-3.jpg']); @@ -249,8 +158,9 @@ test.describe('Image Gallery iDevice', () => { const projectUuid = await createProject(page, 'Image Gallery Controls Test'); await gotoWorkarea(page, projectUuid); - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); // Upload an image await uploadImagesToGallery(page, ['test/fixtures/sample-3.jpg']); @@ -260,23 +170,16 @@ test.describe('Image Gallery iDevice', () => { await expect(imageContainer).toBeVisible({ timeout: 10000 }); // Verify control buttons exist - const attributionBtn = imageContainer.locator('button.attribution'); - const modifyBtn = imageContainer.locator('button.modify'); - const removeBtn = imageContainer.locator('button.remove'); - - await expect(attributionBtn).toBeVisible(); - await expect(modifyBtn).toBeVisible(); - await expect(removeBtn).toBeVisible(); + await expect(imageContainer.locator('button.attribution')).toBeVisible(); + await expect(imageContainer.locator('button.modify')).toBeVisible(); + await expect(imageContainer.locator('button.remove')).toBeVisible(); // Test remove button - await removeBtn.click(); + await imageContainer.locator('button.remove').click(); - // Verify image was removed + // Verify image was removed and "no images" message is shown again await expect(imageContainer).toBeHidden({ timeout: 5000 }); - - // Verify "no images" message is shown again - const noImagesText = page.locator('#textMsxHide'); - await expect(noImagesText).toBeVisible(); + await expect(page.locator('#textMsxHide')).toBeVisible(); }); }); @@ -288,59 +191,31 @@ test.describe('Image Gallery iDevice', () => { const projectUuid = await createProject(page, 'Image Gallery Preview Test'); await gotoWorkarea(page, projectUuid); - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); - // Upload an image + // Upload an image and save await uploadImagesToGallery(page, ['test/fixtures/sample-3.jpg']); - - // Wait for image to be added await page.locator('.imgSelectContainer').first().waitFor({ timeout: 10000 }); + await saveGalleryIdevice(page); - // Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - const saveBtn = block.locator('.btn-save-idevice'); - if ((await saveBtn.count()) > 0) { - await saveBtn.click(); - } - - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); - - // Save project + // Save project and open preview await workarea.save(); await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Wait for iframe to load const iframe = page.frameLocator('#preview-iframe'); await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Verify gallery container exists in preview + // Verify gallery container exists in preview with image elements const previewGallery = iframe.locator('.imageGallery-IDevice'); await expect(previewGallery).toBeVisible({ timeout: 10000 }); - - // Verify image elements exist in preview (even if they can't load due to path issues) - // Note: Images may not load correctly in preview because they're stored in /files/tmp/ - // but the preview iframe may resolve paths differently. This verifies structure, not loading. const previewImages = iframe.locator('.imageGallery-IDevice img'); await expect(previewImages.first()).toBeAttached({ timeout: 10000 }); - - // Verify the image has the expected attributes - const imgSrc = await previewImages.first().getAttribute('src'); - console.log('Preview image src:', imgSrc); - expect(imgSrc).toBeTruthy(); + expect(await previewImages.first().getAttribute('src')).toBeTruthy(); }); }); @@ -352,68 +227,39 @@ test.describe('Image Gallery iDevice', () => { const projectUuid = await createProject(page, 'Image Gallery Lightbox Test'); await gotoWorkarea(page, projectUuid); - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); - // Upload an image + // Upload an image and save await uploadImagesToGallery(page, ['test/fixtures/sample-3.jpg']); - - // Wait for image to be added await page.locator('.imgSelectContainer').first().waitFor({ timeout: 10000 }); + await saveGalleryIdevice(page); - // Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - const saveBtn = block.locator('.btn-save-idevice'); - if ((await saveBtn.count()) > 0) { - await saveBtn.click(); - } - - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); - - // Wait for SimpleLightbox to be loaded + // Wait for SimpleLightbox to be loaded and renderBehaviour to complete await page.waitForFunction(() => typeof (window as any).SimpleLightbox !== 'undefined', undefined, { timeout: 10000, }); - - // Wait for renderBehaviour to complete and SimpleLightbox to initialize await page.waitForTimeout(500); - // Find the gallery link and click on it + // Find the gallery link and verify href is resolved const galleryLink = page.locator('#node-content .imageGallery-IDevice a.imageLink').first(); await expect(galleryLink).toBeVisible({ timeout: 5000 }); - - // Verify the href has been resolved (blob URL or relative path) const href = await galleryLink.getAttribute('href'); // With SW-based preview, assets are served via relative paths (content/resources/...) expect(href).toMatch(/^(blob:|content\/resources\/)/); // Click the image to open lightbox await galleryLink.click(); - - // Wait for SimpleLightbox animation await page.waitForTimeout(500); - // Verify the lightbox overlay is visible + // Verify the lightbox overlay and image are visible const lightboxOverlay = page.locator('.sl-overlay'); await expect(lightboxOverlay).toBeVisible({ timeout: 5000 }); + await expect(page.locator('.sl-image img')).toBeVisible({ timeout: 5000 }); - // Verify the lightbox image is displayed - const lightboxImage = page.locator('.sl-image img'); - await expect(lightboxImage).toBeVisible({ timeout: 5000 }); - - // Close the lightbox by clicking the close button - const closeBtn = page.locator('.sl-close'); - await closeBtn.click(); - - // Verify lightbox is closed + // Close the lightbox and verify it's closed + await page.locator('.sl-close').click(); await expect(lightboxOverlay).toBeHidden({ timeout: 5000 }); }); @@ -427,42 +273,22 @@ test.describe('Image Gallery iDevice', () => { const projectUuid = await createProject(page, 'Image Gallery Preview Lightbox Test'); await gotoWorkarea(page, projectUuid); - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); - // Upload an image + // Upload an image and save await uploadImagesToGallery(page, ['test/fixtures/sample-3.jpg']); - - // Wait for image to be added await page.locator('.imgSelectContainer').first().waitFor({ timeout: 10000 }); + await saveGalleryIdevice(page); - // Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - const saveBtn = block.locator('.btn-save-idevice'); - if ((await saveBtn.count()) > 0) { - await saveBtn.click(); - } - - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); - - // Save project + // Save project and open preview await workarea.save(); await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Wait for iframe to load const iframe = page.frameLocator('#preview-iframe'); await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); @@ -476,8 +302,6 @@ test.describe('Image Gallery iDevice', () => { undefined, { timeout: 15000 }, ); - - // Wait for renderBehaviour to complete await page.waitForTimeout(500); // Click on the image in the preview @@ -486,29 +310,24 @@ test.describe('Image Gallery iDevice', () => { // Verify the href is resolved (blob, data URL, or relative path) const href = await previewGalleryLink.getAttribute('href'); - console.log('Preview gallery link href:', href); // With SW-based preview, assets are served via relative paths (content/resources/...) expect(href).toMatch(/^(blob:|data:|content\/resources\/)/); await previewGalleryLink.click(); - // Wait for SimpleLightbox to open - the wrapper becomes visible with the image - const lightboxWrapper = iframe.locator('.sl-wrapper'); - await expect(lightboxWrapper).toBeVisible({ timeout: 5000 }); + // Wait for SimpleLightbox to open + await expect(iframe.locator('.sl-wrapper')).toBeVisible({ timeout: 5000 }); - // Wait for the lightbox image to have a src set (SimpleLightbox loads asynchronously) + // Verify the lightbox image element exists and has a valid src const lightboxImage = iframe.locator('.sl-image img'); await lightboxImage.waitFor({ state: 'attached', timeout: 5000 }); - - // Verify the lightbox image element exists and has a valid src const imgSrc = await lightboxImage.getAttribute('src'); expect(imgSrc).toBeTruthy(); // With SW-based preview, assets may be relative paths expect(imgSrc).toMatch(/^(blob:|content\/resources\/)/); - // Verify close button is present (closing mechanism exists) - const closeBtn = iframe.locator('.sl-close'); - await expect(closeBtn).toBeVisible({ timeout: 5000 }); + // Verify close button is present + await expect(iframe.locator('.sl-close')).toBeVisible({ timeout: 5000 }); }); }); @@ -520,41 +339,34 @@ test.describe('Image Gallery iDevice', () => { // Capture console errors const consoleErrors: string[] = []; page.on('console', msg => { - if (msg.type() === 'error') { - consoleErrors.push(msg.text()); - } + if (msg.type() === 'error') consoleErrors.push(msg.text()); }); - // 1. Create project and navigate const projectUuid = await createProject(page, 'Image Gallery Modify Test'); await gotoWorkarea(page, projectUuid); - // 2. Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); - // 3. Upload first image + // Upload first image await uploadImagesToGallery(page, ['test/fixtures/sample-2.jpg']); // Verify first image was added const imageContainer = page.locator('.imgSelectContainer').first(); await expect(imageContainer).toBeVisible({ timeout: 10000 }); - // Get the initial image src - should be blob:// or similar valid URL - const initialImage = imageContainer.locator('img.image'); - const initialSrc = await initialImage.getAttribute('src'); - console.log('Initial image src:', initialSrc); - - // 4. Click the "Modify" button on the image + // Click the "Modify" button on the image const modifyBtn = imageContainer.locator('button.modify'); await expect(modifyBtn).toBeVisible(); await modifyBtn.click(); - // 5. Wait for File Manager modal to open + // Wait for File Manager modal to open await page.waitForSelector('#modalFileManager[data-open="true"], #modalFileManager.show', { timeout: 10000, }); - // 6. Upload and select a different image + // Upload and select a different image const fileInput = page.locator('#modalFileManager input[type="file"]').first(); await fileInput.setInputFiles('test/fixtures/sample-3.jpg'); @@ -562,37 +374,24 @@ test.describe('Image Gallery iDevice', () => { await page.waitForSelector('#modalFileManager .media-library-item:not(.media-library-folder)', { timeout: 15000, }); - - // Wait a bit for the UI to update await page.waitForTimeout(500); - // Select the newly uploaded item (last item in grid that's not a folder) - const newItem = page.locator('#modalFileManager .media-library-item:not(.media-library-folder)').last(); - await newItem.click(); + // Select the newly uploaded item and insert + await page.locator('#modalFileManager .media-library-item:not(.media-library-folder)').last().click(); await page.waitForTimeout(300); - - // Click insert button await page.click('#modalFileManager .media-library-insert-btn'); await page.waitForTimeout(500); - // 7. Verify the image src uses valid URL scheme (blob: or relative path, NOT asset://) + // Verify the image src uses valid URL scheme (NOT asset://) const modifiedImage = imageContainer.locator('img.image'); const modifiedSrc = await modifiedImage.getAttribute('src'); - console.log('Modified image src:', modifiedSrc); - - // The src should NOT contain the asset:// protocol which browsers cannot load expect(modifiedSrc).not.toContain('asset://'); - // It should be a valid URL (blob:, data:, or relative/absolute path) expect(modifiedSrc).toMatch(/^(blob:|data:|content\/|\/|https?:\/\/)/); - // 8. Verify no ERR_UNKNOWN_URL_SCHEME errors in console - const schemeErrors = consoleErrors.filter(e => e.includes('ERR_UNKNOWN_URL_SCHEME')); - if (schemeErrors.length > 0) { - console.log('Found URL scheme errors:', schemeErrors); - } - expect(schemeErrors).toHaveLength(0); + // Verify no ERR_UNKNOWN_URL_SCHEME errors in console + expect(consoleErrors.filter(e => e.includes('ERR_UNKNOWN_URL_SCHEME'))).toHaveLength(0); - // 9. Verify image actually loads (naturalWidth > 0) + // Verify image actually loads (naturalWidth > 0) const imageLoaded = await page .waitForFunction( () => { @@ -605,46 +404,21 @@ test.describe('Image Gallery iDevice', () => { .then(() => true) .catch(() => false); expect(imageLoaded).toBe(true); + expect(await modifiedImage.evaluate((el: HTMLImageElement) => el.naturalWidth)).toBeGreaterThan(0); - // Check naturalWidth - const naturalWidth = await modifiedImage.evaluate((el: HTMLImageElement) => el.naturalWidth); - console.log('Modified image naturalWidth:', naturalWidth); - expect(naturalWidth).toBeGreaterThan(0); - - // 10. Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - const saveBtn = block.locator('.btn-save-idevice'); - if ((await saveBtn.count()) > 0) { - await saveBtn.click(); - } - - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); + // Save the iDevice + await saveGalleryIdevice(page); - // Verify the gallery is rendered in view mode + // Verify gallery is rendered in view mode with loaded image const viewModeGallery = page.locator( '#node-content article .idevice_node.image-gallery .imageGallery-IDevice', ); await expect(viewModeGallery).toBeVisible({ timeout: 10000 }); - - // Verify image is displayed in view mode and loads correctly const viewModeImage = viewModeGallery.locator('img').first(); await expect(viewModeImage).toBeVisible({ timeout: 5000 }); - - // Wait for image to load and verify it loaded correctly await page.waitForTimeout(500); - const viewModeNaturalWidth = await viewModeImage.evaluate((el: HTMLImageElement) => el.naturalWidth); - console.log('View mode image naturalWidth:', viewModeNaturalWidth); - expect(viewModeNaturalWidth).toBeGreaterThan(0); + expect(await viewModeImage.evaluate((el: HTMLImageElement) => el.naturalWidth)).toBeGreaterThan(0); - // Save project await workarea.save(); }); }); @@ -657,44 +431,26 @@ test.describe('Image Gallery iDevice', () => { // Capture console errors const consoleErrors: string[] = []; page.on('console', msg => { - if (msg.type() === 'error') { - consoleErrors.push(msg.text()); - } + if (msg.type() === 'error') consoleErrors.push(msg.text()); }); - // Create project const projectUuid = await createProject(page, 'Image Gallery Persistence Test'); await gotoWorkarea(page, projectUuid); - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); // Upload images await uploadImagesToGallery(page, ['test/fixtures/sample-3.jpg']); - // Verify image container exists + // Verify image container exists with a src attribute const imageContainer = page.locator('.imgSelectContainer').first(); await expect(imageContainer).toBeVisible({ timeout: 10000 }); - - // Verify the image was added successfully (check for src attribute) - const galleryImage = imageContainer.locator('img.image'); - const srcAttr = await galleryImage.getAttribute('src'); - expect(srcAttr).toBeTruthy(); - console.log('Image src before save:', srcAttr); + expect(await imageContainer.locator('img.image').getAttribute('src')).toBeTruthy(); // Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - await block.locator('.btn-save-idevice').click(); - - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); + await saveGalleryIdevice(page); // Verify the gallery is rendered in view mode const viewModeGallery = page.locator( @@ -702,95 +458,45 @@ test.describe('Image Gallery iDevice', () => { ); await expect(viewModeGallery).toBeVisible({ timeout: 10000 }); - // Get the iDevice data from Yjs before reload to verify no blob URLs are stored + // Verify that no blob URLs are stored in the iDevice Yjs data const ideviceDataBeforeReload = await page.evaluate(() => { const bridge = (window as any).eXeLearning?.app?.project?._yjsBridge; if (!bridge) return null; - - // Get all idevices const pages = bridge.documentManager?.getPages?.(); if (!pages) return null; - for (const pageData of pages) { - const blocks = pageData?.blocks; - if (!blocks) continue; - for (const block of blocks) { - const idevices = block?.idevices; - if (!idevices) continue; - for (const idevice of idevices) { - if (idevice?.type === 'image-gallery') { - return idevice.data; - } + for (const block of pageData?.blocks ?? []) { + for (const idevice of block?.idevices ?? []) { + if (idevice?.type === 'image-gallery') return idevice.data; } } } return null; }); - console.log('iDevice data before reload:', JSON.stringify(ideviceDataBeforeReload, null, 2)); - - // Verify that no blob URLs are stored in the iDevice data if (ideviceDataBeforeReload) { - const dataString = JSON.stringify(ideviceDataBeforeReload); - expect(dataString).not.toContain('blob:'); + expect(JSON.stringify(ideviceDataBeforeReload)).not.toContain('blob:'); } - // Save the project to persist Yjs document + // Save the project and reload await workarea.save(); - await page.waitForTimeout(500); // Wait for save to complete - - // Reload the page - await reloadPage(page); - - // Wait for the tree to be populated (using role="tree" which is more reliable) - await page.waitForSelector('[role="tree"]', { timeout: 15000 }); - - // Navigate to the page containing the image gallery using multiple selector strategies - const pageNodeSelectors = [ - '[role="treeitem"] button:has-text("New page")', - '[role="treeitem"] button:has-text("Nueva página")', - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[role="treeitem"]:not([selected]) button', - '#menu_structure .structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - console.log('Page selected using selector:', selector); - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - throw new Error('Could not select page node after reload'); - } - await page.waitForTimeout(500); + await reloadPage(page); + await selectFirstPage(page); - // Verify the image gallery is rendered (view mode, not edition) + // Verify the image gallery renders correctly after reload const galleryViewAfterReload = page.locator('#node-content .idevice_node.image-gallery').first(); await expect(galleryViewAfterReload).toBeVisible({ timeout: 15000 }); - // Verify the gallery container exists const viewModeGalleryAfterReload = page.locator( '#node-content .idevice_node.image-gallery .imageGallery-IDevice', ); await expect(viewModeGalleryAfterReload).toBeVisible({ timeout: 10000 }); - // Verify images are visible (not broken) + // Verify images are visible and loaded (naturalWidth > 0) const galleryImages = viewModeGalleryAfterReload.locator('img'); await expect(galleryImages.first()).toBeVisible({ timeout: 10000 }); - // Wait for image to load and verify it loaded correctly (naturalWidth > 0) const imageLoaded = await page .waitForFunction( () => { @@ -804,26 +510,13 @@ test.describe('Image Gallery iDevice', () => { ) .then(() => true) .catch(() => false); - expect(imageLoaded).toBe(true); - const naturalWidth = await galleryImages.first().evaluate((el: HTMLImageElement) => el.naturalWidth); - console.log('Image naturalWidth after reload:', naturalWidth); - expect(naturalWidth).toBeGreaterThan(0); - - // Verify image src is NOT a blob URL (should be resolved by AssetManager) - const imageSrc = await galleryImages.first().getAttribute('src'); - console.log('Image src after reload:', imageSrc); - // After reload, the AssetManager should resolve asset:// URLs to blob: URLs for display - // OR the image may use a relative path. Either way, it should NOT be a stale blob URL from before. - expect(imageSrc).toBeTruthy(); + expect(await galleryImages.first().evaluate((el: HTMLImageElement) => el.naturalWidth)).toBeGreaterThan(0); + expect(await galleryImages.first().getAttribute('src')).toBeTruthy(); // Verify NO ERR_FILE_NOT_FOUND errors for blob URLs - const blobErrors = consoleErrors.filter(e => e.includes('ERR_FILE_NOT_FOUND') && e.includes('blob:')); - if (blobErrors.length > 0) { - console.log('Found blob URL errors:', blobErrors); - } - expect(blobErrors).toHaveLength(0); + expect(consoleErrors.filter(e => e.includes('ERR_FILE_NOT_FOUND') && e.includes('blob:'))).toHaveLength(0); }); test('should persist modified images correctly after multiple page reloads', async ({ @@ -833,69 +526,31 @@ test.describe('Image Gallery iDevice', () => { const page = authenticatedPage; const workarea = new WorkareaPage(page); - // Capture console logs and errors + // Capture console errors const consoleErrors: string[] = []; - const consoleLogs: string[] = []; page.on('console', msg => { - const text = msg.text(); - if (msg.type() === 'error') { - consoleErrors.push(text); - } - // Capture Image Gallery related logs - if (text.includes('[Image Gallery')) { - consoleLogs.push(`[${msg.type()}] ${text}`); - console.log(`[Browser] ${text}`); - } + if (msg.type() === 'error') consoleErrors.push(msg.text()); }); // Helper function to wait for app initialization and select page async function waitForAppAndSelectPage(): Promise { await waitForAppReady(page); + await selectFirstPage(page); - // Wait for the tree to be populated - await page.waitForSelector('[role="tree"]', { timeout: 15000 }); - - // Navigate to the page containing the image gallery - const pageNodeSelectors = [ - '[role="treeitem"] button:has-text("New page")', - '[role="treeitem"] button:has-text("Nueva página")', - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - ]; - - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - break; - } catch { - // Try next selector - } - } - } - - // Wait for the image gallery iDevice to be visible in the content area + // Wait for the image gallery iDevice to be visible in view mode await page.waitForSelector('#node-content .idevice_node.image-gallery', { timeout: 15000 }); - - // Wait for the gallery to render in view mode (not edition mode) await page.waitForFunction( () => { const idevice = document.querySelector('#node-content .idevice_node.image-gallery'); if (!idevice) return false; - // Wait for view mode (either no mode attribute or mode !== 'edition') - const mode = idevice.getAttribute('mode'); - if (mode === 'edition') return false; - // Wait for the gallery content to be rendered - const gallery = idevice.querySelector('.imageGallery-IDevice'); - return !!gallery; + if (idevice.getAttribute('mode') === 'edition') return false; + return !!idevice.querySelector('.imageGallery-IDevice'); }, undefined, { timeout: 15000 }, ); - // Wait for AssetManager to resolve asset URLs to blob URLs - // Increase wait time to ensure all assets are fully resolved + // Wait for AssetManager to resolve asset URLs await page.waitForTimeout(500); } @@ -907,24 +562,9 @@ test.describe('Image Gallery iDevice', () => { const galleryImages = viewModeGallery.locator('img'); await expect(galleryImages).toHaveCount(expectedCount, { timeout: 10000 }); - // Verify each image loads correctly (naturalWidth > 0) for (let i = 0; i < expectedCount; i++) { - const img = galleryImages.nth(i); - await expect(img).toBeVisible({ timeout: 10000 }); - - // Add debugging: check attributes to catch issues early - const debugInfo = await img.evaluate((el: HTMLImageElement) => ({ - src: el.getAttribute('src'), - origin: el.getAttribute('origin'), - naturalWidth: el.naturalWidth, - complete: el.complete, - })); - console.log(`Image ${i + 1} attributes:`, debugInfo); - - // In view mode, images use asset:// URLs in src which get resolved by the asset resolver - - // Wait for image to load successfully (naturalWidth > 0) - // This polls until the image is loaded, not just complete + await expect(galleryImages.nth(i)).toBeVisible({ timeout: 10000 }); + const result = await page .waitForFunction( (idx: number) => { @@ -933,7 +573,6 @@ test.describe('Image Gallery iDevice', () => { ); const img = images[idx] as HTMLImageElement; if (!img) return null; - // Only return when image is actually loaded with content if (img.complete && img.naturalWidth > 0) { return { loaded: true, @@ -941,8 +580,6 @@ test.describe('Image Gallery iDevice', () => { src: img.getAttribute('src')?.substring(0, 100), }; } - // If image is complete but has no width, it failed to load - // Keep polling - AssetManager may still be resolving the URL return null; }, i, @@ -950,7 +587,6 @@ test.describe('Image Gallery iDevice', () => { ) .then(handle => handle.jsonValue()) .catch(async () => { - // On timeout, get current state for debugging const debugState = await page.evaluate((idx: number) => { const images = document.querySelectorAll( '#node-content .idevice_node.image-gallery .imageGallery-IDevice img', @@ -968,7 +604,6 @@ test.describe('Image Gallery iDevice', () => { return debugState; }); - console.log(`Image ${i + 1}:`, result); expect(result?.loaded).toBe(true); expect(result?.naturalWidth).toBeGreaterThan(0); } @@ -992,16 +627,11 @@ test.describe('Image Gallery iDevice', () => { undefined, { timeout: 15000 }, ); - // Wait for MutationObserver to process images and set data-asset-url attributes - // This ensures all asset URLs are properly resolved before any modifications await page.waitForTimeout(500); // Click modify button on the specified image - const imageContainers = page.locator('.imgSelectContainer'); - const targetContainer = imageContainers.nth(index); - const modifyBtn = targetContainer.locator('button.modify'); - await modifyBtn.click(); + await page.locator('.imgSelectContainer').nth(index).locator('button.modify').click(); // Wait for File Manager modal to open await page.waitForSelector('#modalFileManager[data-open="true"], #modalFileManager.show', { @@ -1009,39 +639,19 @@ test.describe('Image Gallery iDevice', () => { }); // Upload and select the new image - const fileInput = page.locator('#modalFileManager input[type="file"]').first(); - await fileInput.setInputFiles(newImagePath); - - // Wait for upload to complete + await page.locator('#modalFileManager input[type="file"]').first().setInputFiles(newImagePath); await page.waitForSelector('#modalFileManager .media-library-item:not(.media-library-folder)', { timeout: 15000, }); await page.waitForTimeout(500); - // Select the newly uploaded item - const newItem = page.locator('#modalFileManager .media-library-item:not(.media-library-folder)').last(); - await newItem.click(); + await page.locator('#modalFileManager .media-library-item:not(.media-library-folder)').last().click(); await page.waitForTimeout(300); - - // Click insert button await page.click('#modalFileManager .media-library-insert-btn'); await page.waitForTimeout(500); - // Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - await block.locator('.btn-save-idevice').click(); - - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); - - // Save the project + // Save the iDevice and project + await saveGalleryIdevice(page); await workarea.save(); await page.waitForTimeout(500); } @@ -1050,72 +660,40 @@ test.describe('Image Gallery iDevice', () => { const projectUuid = await createProject(page, 'Image Gallery Multi-Modify Test'); await gotoWorkarea(page, projectUuid); - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); - // Upload two images await uploadImagesToGallery(page, ['test/fixtures/sample-2.jpg', 'test/fixtures/sample-3.jpg']); + await expect(page.locator('.imgSelectContainer')).toHaveCount(2, { timeout: 10000 }); - // Verify two image containers exist - const imageContainers = page.locator('.imgSelectContainer'); - await expect(imageContainers).toHaveCount(2, { timeout: 10000 }); - - // Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - await block.locator('.btn-save-idevice').click(); - - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); - - // Save project + await saveGalleryIdevice(page); await workarea.save(); await page.waitForTimeout(500); - console.log('Step 1 complete: Two images added and saved'); // ============ STEP 2: Reload and verify both images display ============ await reloadPage(page); await waitForAppAndSelectPage(); - await verifyImagesLoadCorrectly(2); - console.log('Step 2 complete: Both images display correctly after first reload'); // ============ STEP 3: Modify first image ============ - // We'll use sample-3.jpg to replace the first image await modifyImageAtIndex(0, 'test/fixtures/sample-3.jpg'); - console.log('Step 3 complete: First image modified'); // ============ STEP 4: Reload and verify both images still display ============ await reloadPage(page); await waitForAppAndSelectPage(); - await verifyImagesLoadCorrectly(2); - console.log('Step 4 complete: Both images display correctly after modifying first image'); // ============ STEP 5: Modify second image ============ await modifyImageAtIndex(1, 'test/fixtures/sample-2.jpg'); - console.log('Step 5 complete: Second image modified'); // ============ STEP 6: Final reload and verification ============ await reloadPage(page); await waitForAppAndSelectPage(); - await verifyImagesLoadCorrectly(2); // Verify NO ERR_FILE_NOT_FOUND errors for blob URLs - const blobErrors = consoleErrors.filter(e => e.includes('ERR_FILE_NOT_FOUND') && e.includes('blob:')); - if (blobErrors.length > 0) { - console.log('Found blob URL errors:', blobErrors); - } - expect(blobErrors).toHaveLength(0); - - console.log('Step 6 complete: Both images display correctly after final reload, no console errors'); + expect(consoleErrors.filter(e => e.includes('ERR_FILE_NOT_FOUND') && e.includes('blob:'))).toHaveLength(0); }); }); @@ -1124,13 +702,10 @@ test.describe('Image Gallery iDevice', () => { * Helper to open file manager from image gallery "Add images" button */ async function openFileManagerFromGallery(page: Page): Promise { - // Click the "Add images" button which opens file manager await page.click('#addImageButton'); - // Wait for file manager modal to open await page.waitForSelector('#modalFileManager[data-open="true"], #modalFileManager.show', { timeout: 10000, }); - // Wait for the grid to be ready await page.waitForTimeout(500); } @@ -1138,15 +713,10 @@ test.describe('Image Gallery iDevice', () => { * Helper to create a folder in file manager */ async function createFolderInFileManager(page: Page, folderName: string): Promise { - // Handle the prompt dialog that appears page.once('dialog', async dialog => { await dialog.accept(folderName); }); - - // Click new folder button await page.click('.media-library-newfolder-btn'); - - // Wait for folder to appear in grid await page.waitForSelector(`.media-library-folder[data-folder-name="${folderName}"]`, { timeout: 5000 }); } @@ -1154,52 +724,25 @@ test.describe('Image Gallery iDevice', () => { * Helper to navigate into a folder by double-clicking */ async function navigateToFolder(page: Page, folderName: string): Promise { - const folderItem = page.locator(`.media-library-folder[data-folder-name="${folderName}"]`); - await folderItem.dblclick(); - // Wait for navigation to complete + await page.locator(`.media-library-folder[data-folder-name="${folderName}"]`).dblclick(); await page.waitForTimeout(500); } /** - * Helper to upload an image and select it + * Helper to upload an image and select it in the file manager */ async function uploadAndSelectImage(page: Page, fixturePath: string): Promise { - // Find the upload input - const fileInput = page.locator('#modalFileManager input[type="file"]').first(); - await fileInput.setInputFiles(fixturePath); - - // Wait for upload to complete and item to appear + await page.locator('#modalFileManager input[type="file"]').first().setInputFiles(fixturePath); await page.waitForSelector('#modalFileManager .media-library-item:not(.media-library-folder)', { timeout: 15000, }); - - // Wait a bit for the UI to update await page.waitForTimeout(500); - - // Select the newly uploaded item (last item in grid that's not a folder) - const newItem = page.locator('#modalFileManager .media-library-item:not(.media-library-folder)').last(); - await newItem.click(); + await page.locator('#modalFileManager .media-library-item:not(.media-library-folder)').last().click(); await page.waitForTimeout(300); - - // Click insert button await page.click('#modalFileManager .media-library-insert-btn'); await page.waitForTimeout(500); } - /** - * Helper to navigate to a specific breadcrumb path - */ - async function navigateToBreadcrumbRoot(page: Page): Promise { - // Click on the root breadcrumb item to go back to root - const rootBreadcrumb = page.locator( - '.media-library-breadcrumbs .breadcrumb-item[data-path=""], .media-library-breadcrumbs .breadcrumb-root', - ); - if ((await rootBreadcrumb.count()) > 0) { - await rootBreadcrumb.first().click(); - await page.waitForTimeout(500); - } - } - test('should load images from different folder depths in preview', async ({ authenticatedPage, createProject, @@ -1210,14 +753,13 @@ test.describe('Image Gallery iDevice', () => { const projectUuid = await createProject(page, 'Image Gallery Folder Depth Test'); await gotoWorkarea(page, projectUuid); - // Add image-gallery iDevice - await addImageGalleryFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'image-gallery'); // 1. Add image from ROOT level via file manager await openFileManagerFromGallery(page); await uploadAndSelectImage(page, 'test/fixtures/sample-2.jpg'); - - // Verify first image was added const imageContainers = page.locator('.imgSelectContainer'); await expect(imageContainers).toHaveCount(1, { timeout: 10000 }); @@ -1226,8 +768,6 @@ test.describe('Image Gallery iDevice', () => { await createFolderInFileManager(page, 'photos'); await navigateToFolder(page, 'photos'); await uploadAndSelectImage(page, 'test/fixtures/sample-3.jpg'); - - // Verify second image was added await expect(imageContainers).toHaveCount(2, { timeout: 10000 }); // 3. Add image from NESTED folders (2 levels: photos/vacation) @@ -1237,26 +777,10 @@ test.describe('Image Gallery iDevice', () => { await navigateToFolder(page, 'vacation'); // Reuse sample-2.jpg - we're testing path handling, not unique images await uploadAndSelectImage(page, 'test/fixtures/sample-2.jpg'); - - // Verify third image was added await expect(imageContainers).toHaveCount(3, { timeout: 15000 }); - // Save the iDevice - const block = page.locator('#node-content article .idevice_node.image-gallery').first(); - const saveBtn = block.locator('.btn-save-idevice'); - await saveBtn.click(); - - // Wait for edition mode to end - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.image-gallery'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 15000 }, - ); - - // Save project + // Save the iDevice and project + await saveGalleryIdevice(page); await workarea.save(); await page.waitForTimeout(500); @@ -1264,29 +788,22 @@ test.describe('Image Gallery iDevice', () => { const previewLoaded = await waitForPreviewContent(page, 30000); expect(previewLoaded).toBe(true); - // Get iframe reference const iframe = getPreviewFrame(page); - // Verify gallery exists in preview - const previewGallery = iframe.locator('.imageGallery-IDevice'); - await expect(previewGallery).toBeVisible({ timeout: 10000 }); - - // Verify ALL 3 images exist in preview + // Verify gallery exists in preview with all 3 images + await expect(iframe.locator('.imageGallery-IDevice')).toBeVisible({ timeout: 10000 }); const previewImages = iframe.locator('.imageGallery-IDevice img'); await expect(previewImages).toHaveCount(3, { timeout: 10000 }); // Check each image loads with naturalWidth > 0 for (let i = 0; i < 3; i++) { - const img = previewImages.nth(i); - await img.waitFor({ state: 'attached', timeout: 10000 }); + await previewImages.nth(i).waitFor({ state: 'attached', timeout: 10000 }); - // Wait for image to load and check naturalWidth const result = await iframe.locator('body').evaluate(async (_, idx: number) => { const images = document.querySelectorAll('.imageGallery-IDevice img'); const img = images[idx] as HTMLImageElement; if (!img) return { loaded: false, src: '', naturalWidth: 0 }; - // Wait for load if not already loaded if (!img.complete) { await new Promise((resolve, reject) => { img.onload = resolve; @@ -1302,7 +819,6 @@ test.describe('Image Gallery iDevice', () => { }; }, i as number); - console.log(`Image ${i + 1}:`, result); expect(result.loaded).toBe(true); expect(result.naturalWidth).toBeGreaterThan(0); } diff --git a/test/e2e/playwright/specs/idevices/interactive-video.spec.ts b/test/e2e/playwright/specs/idevices/interactive-video.spec.ts index da3ebb59f..b90375122 100644 --- a/test/e2e/playwright/specs/idevices/interactive-video.spec.ts +++ b/test/e2e/playwright/specs/idevices/interactive-video.spec.ts @@ -1,5 +1,12 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { waitForAppReady, reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + waitForAppReady, + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; import type { Page, FrameLocator } from '@playwright/test'; @@ -11,161 +18,54 @@ import type { Page, FrameLocator } from '@playwright/test'; * - Opening the interactive video editor * - Creating a cover (frontpage) * - Creating slides with different content types - * - Saving editor changes + * - Saving editor changes and persistence after reload * - Preview rendering + * - Configuration API */ const TEST_DATA = { - projectTitle: 'Interactive Video E2E Test Project', videoFixture: 'test/fixtures/sample-video-480-900kb.webm', coverTitle: 'Welcome to Interactive Video', coverIntro: 'This is an interactive video with slides and questions.', textSlideContent: '

This is a text slide with important information.

', questionText: 'What is 2 + 2?', questionAnswers: ['3', '4', '5', '6'], - correctAnswer: 1, // Index of correct answer (0-based) = "4" + correctAnswer: 1, }; -/** - * Helper to select a page in the navigation tree (required before adding iDevices) - */ -async function selectPageNode(page: Page): Promise { - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - await page.waitForTimeout(500); - - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => {}); -} - -/** - * Helper to add an Interactive Video iDevice by expanding the category and clicking the iDevice - */ -async function addInteractiveVideoIdeviceFromPanel(page: Page): Promise { - await selectPageNode(page); - - // Expand "Assessment and tracking" category (or "Evaluación" in Spanish) - const assessmentCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Assessment|Evaluación/i }), - }) - .first(); - - if ((await assessmentCategory.count()) > 0) { - const isCollapsed = await assessmentCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - const label = assessmentCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - await page.waitForTimeout(500); - - // Find and click the Interactive Video iDevice - const interactiveVideoIdevice = page.locator('.idevice_item[id="interactive-video"]').first(); - await interactiveVideoIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await interactiveVideoIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.interactive-video').first().waitFor({ timeout: 15000 }); - - // Wait for the form to be created - await page.waitForTimeout(500); - - // Wait for the file input to be visible - await page - .waitForFunction( - () => { - const fileInput = document.querySelector('#interactiveVideoFile'); - return fileInput !== null; - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => {}); -} - /** * Helper to upload a video file via the file picker */ async function uploadVideoFile(page: Page, fixturePath: string): Promise { - // Ensure "Local file" type is selected const localRadio = page.locator('#interactiveVideoType-local'); await localRadio.check(); await page.waitForTimeout(300); - // Click the file picker button const fileInput = page.locator('#interactiveVideoFile'); await fileInput.waitFor({ state: 'visible', timeout: 5000 }); - // Find the associated pick button const pickButton = page.locator('#interactiveVideoFile + .exe-pick-any-file, #interactiveVideoFile + button'); - if ((await pickButton.count()) > 0) { await pickButton.first().click(); } else { await fileInput.click(); } - // Wait for Media Library modal to appear await page.waitForSelector('#modalFileManager[data-open="true"], #modalFileManager.show', { timeout: 10000 }); - // Upload the video file const uploadInput = page.locator('#modalFileManager .media-library-upload-input'); await uploadInput.setInputFiles(fixturePath); - // Wait for upload to complete and item to appear const mediaItem = page.locator('#modalFileManager .media-library-item').first(); - await mediaItem.waitFor({ state: 'visible', timeout: 30000 }); // Longer timeout for video - - // Click on the uploaded item to select it + await mediaItem.waitFor({ state: 'visible', timeout: 30000 }); await mediaItem.click(); - await page.waitForTimeout(500); + await page.waitForTimeout(300); - // Click insert button const insertBtn = page.locator( '#modalFileManager .media-library-insert-btn, #modalFileManager button:has-text("Insert"), #modalFileManager button:has-text("Insertar")', ); await insertBtn.first().click(); - // Wait for modal to close await page.waitForFunction( () => { const modal = document.querySelector('#modalFileManager'); @@ -174,8 +74,6 @@ async function uploadVideoFile(page: Page, fixturePath: string): Promise { undefined, { timeout: 10000 }, ); - - await page.waitForTimeout(500); } /** @@ -187,7 +85,7 @@ async function closeAlertModals(page: Page): Promise { const okBtn = modal.locator('button:has-text("OK"), button:has-text("Aceptar"), .btn-primary').first(); if ((await okBtn.count()) > 0) { await okBtn.click(); - await page.waitForTimeout(500); + await page.waitForTimeout(300); } } } @@ -208,10 +106,6 @@ async function saveInteractiveVideoIdevice(page: Page): Promise { await saveBtn.click(); } - // Wait for save to complete - await page.waitForTimeout(500); - - // Wait for edition mode to end await page .waitForFunction( () => { @@ -222,31 +116,21 @@ async function saveInteractiveVideoIdevice(page: Page): Promise { { timeout: 10000 }, ) .catch(() => {}); - - await page.waitForTimeout(500); } /** * Helper to open the interactive video editor and wait for it to load */ async function openVideoEditor(page: Page): Promise { - // Click the Editor button const editorBtn = page.locator('#interactiveVideoOpenEditor'); await editorBtn.waitFor({ state: 'visible', timeout: 5000 }); await editorBtn.click(); - // Wait for the editor modal to become visible await page.waitForSelector('#modalGenericIframeContainer.show', { state: 'visible', timeout: 10000 }); - await page.waitForTimeout(500); - // Get the iframe inside the modal const editorIframe = page.frameLocator('#modalGenericIframeContainer iframe'); - - // Wait for the editor to initialize await editorIframe.locator('#admin-content').waitFor({ state: 'attached', timeout: 15000 }); - // Wait for the iframe body to be visible (starts with display:none, becomes visible after scripts load) - // This is critical for Firefox which may be slower to initialize scripts await page.waitForFunction( () => { const iframe = document.querySelector('#modalGenericIframeContainer iframe') as HTMLIFrameElement; @@ -258,11 +142,8 @@ async function openVideoEditor(page: Page): Promise { { timeout: 15000 }, ); - // Wait for the controls to be visible (frontpage-link is inside #controls) await editorIframe.locator('#controls').waitFor({ state: 'visible', timeout: 10000 }); - - // Give time for TinyMCE and other components to initialize - await page.waitForTimeout(500); + await page.waitForTimeout(300); return editorIframe; } @@ -271,22 +152,12 @@ async function openVideoEditor(page: Page): Promise { * Helper to create a cover (frontpage) in the editor */ async function createCover(page: Page, editorIframe: FrameLocator, title: string, intro: string): Promise { - // Wait for the frontpage link to be ready and visible const coverLink = editorIframe.locator('#frontpage-link'); await coverLink.waitFor({ state: 'visible', timeout: 10000 }); - - // Click using JavaScript to ensure the jQuery click handler fires in Firefox - // Firefox sometimes has issues with native click events on elements with jQuery handlers await coverLink.evaluate(el => (el as HTMLElement).click()); - // Wait for jQuery fadeIn() animation to complete (default 400ms) - // Firefox needs this buffer as it may detect element as hidden during the animation await page.waitForTimeout(500); - // Wait for frontpage block to appear - const frontpageBlock = editorIframe.locator('#frontpage-block'); - - // Poll for visibility - fadeIn changes display and opacity await page.waitForFunction( () => { const iframe = document.querySelector('#modalGenericIframeContainer iframe') as HTMLIFrameElement; @@ -299,17 +170,12 @@ async function createCover(page: Page, editorIframe: FrameLocator, title: string { timeout: 15000 }, ); - // Wait a bit for the block to fully render await editorIframe.locator('#frontpage-title').waitFor({ state: 'visible', timeout: 5000 }); - // Fill in the title (required) const titleInput = editorIframe.locator('#frontpage-title'); await titleInput.clear(); await titleInput.fill(title); - // Wait for TinyMCE to initialize (the textarea is replaced by TinyMCE) - // TinyMCE creates a wrapper with class .tox-tinymce containing the iframe - // First wait for the TinyMCE wrapper to appear near the content field const tinyMceWrapper = editorIframe .locator('#frontpage-content') .locator('xpath=..') @@ -319,28 +185,20 @@ async function createCover(page: Page, editorIframe: FrameLocator, title: string const tinyMceIframe = tinyMceWrapper.locator('iframe').first(); await tinyMceIframe.waitFor({ state: 'visible', timeout: 5000 }); - // Fill in the introduction through TinyMCE iframe const tinyMceBody = tinyMceIframe.contentFrame().locator('body'); await tinyMceBody.click(); await tinyMceBody.fill(intro); + await tinyMceBody.page().waitForTimeout(300); - // Give TinyMCE time to process the input - await tinyMceBody.page().waitForTimeout(500); - - // Submit the cover form const submitBtn = editorIframe.locator('#frontpage-submit'); await submitBtn.waitFor({ state: 'visible', timeout: 5000 }); await submitBtn.click(); - // Wait for success message to appear (Cover Updated message) - // The message shows in #frontpage-form-msg for 1 second before fading out try { await editorIframe.locator('#frontpage-form-msg').waitFor({ state: 'visible', timeout: 5000 }); - // Wait for the message to be processed - await editorIframe.locator('#frontpage-form-msg').page().waitForTimeout(500); + await editorIframe.locator('#frontpage-form-msg').page().waitForTimeout(300); } catch { - // Even if message doesn't show, wait a bit for the save to complete - await editorIframe.locator('#frontpage-block').page().waitForTimeout(500); + await editorIframe.locator('#frontpage-block').page().waitForTimeout(300); } } @@ -348,18 +206,14 @@ async function createCover(page: Page, editorIframe: FrameLocator, title: string * Helper to create a text slide in the editor */ async function createTextSlide(editorIframe: FrameLocator, content: string): Promise { - // Click on Create link to go to add-block const createLink = editorIframe.locator('a[href="#add-block"]'); await createLink.click(); await editorIframe.locator('#add-block').waitFor({ state: 'visible', timeout: 5000 }); - // Text type should be selected by default const textLink = editorIframe.locator('a[href="#text-block"]'); await textLink.click(); await editorIframe.locator('#text-block').waitFor({ state: 'visible', timeout: 5000 }); - // Wait for TinyMCE to initialize - // TinyMCE creates a wrapper with class .tox-tinymce containing the iframe const tinyMceWrapper = editorIframe .locator('#text-block-content') .locator('xpath=..') @@ -369,100 +223,43 @@ async function createTextSlide(editorIframe: FrameLocator, content: string): Pro const tinyMceIframe = tinyMceWrapper.locator('iframe').first(); await tinyMceIframe.waitFor({ state: 'visible', timeout: 5000 }); - // Fill in the text content through TinyMCE iframe const tinyMceBody = tinyMceIframe.contentFrame().locator('body'); await tinyMceBody.click(); await tinyMceBody.fill(content); - // Submit the text block const submitBtn = editorIframe.locator('#text-block-submit'); await submitBtn.click(); - - // Wait for success message await editorIframe .locator('#text-block-msg') .waitFor({ state: 'visible', timeout: 5000 }) .catch(() => {}); } -/** - * Helper to create a single choice question slide in the editor - */ -async function createSingleChoiceSlide( - editorIframe: FrameLocator, - question: string, - answers: string[], - correctIndex: number, -): Promise { - // Click on Create link to go to add-block - const createLink = editorIframe.locator('a[href="#add-block"]'); - await createLink.click(); - await editorIframe.locator('#add-block').waitFor({ state: 'visible', timeout: 5000 }); - - // Click on Single Choice type - const singleChoiceLink = editorIframe.locator('a[href="#singleChoice-block"]'); - await singleChoiceLink.click(); - await editorIframe.locator('#singleChoice-block').waitFor({ state: 'visible', timeout: 5000 }); - - // Fill in the question (in Question tab) - const questionInput = editorIframe.locator('#singleChoice-question'); - await questionInput.fill(question); - - // Switch to Answers tab - const answersTab = editorIframe.locator('a[href="#singleChoice-b"]'); - await answersTab.click(); - await editorIframe.locator('#singleChoice-b').waitFor({ state: 'visible', timeout: 5000 }); - - // Fill in answers - for (let i = 0; i < Math.min(answers.length, 6); i++) { - const answerInput = editorIframe.locator(`#singleChoice-answer-${i + 1}`); - await answerInput.fill(answers[i]); - } - - // Mark the correct answer - const correctRadio = editorIframe.locator(`#singleChoice-answer-${correctIndex + 1}-right`); - await correctRadio.check(); - - // Submit the single choice block - const submitBtn = editorIframe.locator('#singleChoice-block-submit'); - await submitBtn.click(); - - // Wait for success message - await editorIframe.locator('#singleChoice-block-msg').waitFor({ state: 'visible', timeout: 5000 }); -} - /** * Helper to save the editor and close it */ async function saveAndCloseEditor(page: Page, editorIframe: FrameLocator): Promise { - // Click the Save link in the actions menu (using CSS class selector to avoid translation issues) const saveLink = editorIframe.locator('#actions li.save a').first(); await saveLink.waitFor({ state: 'visible', timeout: 5000 }); await saveLink.click(); + await page.waitForTimeout(300); - // Wait for save to complete - await page.waitForTimeout(500); - - // If there's an Accept button visible, click it const acceptBtn = editorIframe .locator('button') .filter({ hasText: /Accept|Aceptar/i }) .first(); if (await acceptBtn.isVisible()) { await acceptBtn.click(); - await page.waitForTimeout(500); + await page.waitForTimeout(300); } - // Set up handler for the confirm dialog that will appear when clicking Exit page.once('dialog', async dialog => { await dialog.accept(); }); - // Click the Exit link in the actions menu (using CSS class selector) const exitLink = editorIframe.locator('#actions li.exit a').first(); await exitLink.click(); - // Wait for the modal to close (either removed from DOM or hidden) await page.waitForFunction( () => { const modal = document.querySelector('#modalGenericIframeContainer'); @@ -473,185 +270,106 @@ async function saveAndCloseEditor(page: Page, editorIframe: FrameLocator): Promi undefined, { timeout: 15000 }, ); - - await page.waitForTimeout(500); } test.describe('Interactive Video iDevice', () => { test.describe('Basic Operations', () => { - test('should add interactive-video iDevice to page', async ({ authenticatedPage, createProject }) => { + test('should add iDevice, upload video, and save', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'Interactive Video Add Test'); + const projectUuid = await createProject(page, 'Interactive Video Workflow Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add an Interactive Video iDevice - await addInteractiveVideoIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'interactive-video'); - // Verify iDevice was added + // Wait for the file input to be ready + await page + .waitForFunction(() => document.querySelector('#interactiveVideoFile') !== null, undefined, { + timeout: 10000, + }) + .catch(() => {}); + + // Verify iDevice was added with form elements const interactiveVideoIdevice = page .locator('#node-content article .idevice_node.interactive-video') .first(); await expect(interactiveVideoIdevice).toBeVisible({ timeout: 10000 }); + await expect(page.locator('#interactiveVideoFile')).toBeVisible({ timeout: 5000 }); - // Verify the form is visible with file input - const fileInput = page.locator('#interactiveVideoFile'); - await expect(fileInput).toBeVisible({ timeout: 5000 }); - }); - - test('should upload local video file and save iDevice', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'Interactive Video Upload Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add iDevice - await addInteractiveVideoIdeviceFromPanel(page); - - // Upload video file + // Upload video file and verify asset:// URL await uploadVideoFile(page, TEST_DATA.videoFixture); - - // Verify the file path is set with asset:// URL format (uuid.ext) - const fileInput = page.locator('#interactiveVideoFile'); - const filePath = await fileInput.inputValue(); + const filePath = await page.locator('#interactiveVideoFile').inputValue(); expect(filePath.startsWith('asset://')).toBe(true); - // Should have .webm extension (from original file) expect(filePath).toMatch(/^asset:\/\/[a-f0-9-]+\.webm$/); - // Save the iDevice + // Save and verify view mode shows the video container await saveInteractiveVideoIdevice(page); + await expect(page.locator('#node-content .interactive-video .exe-interactive-video')).toBeAttached({ + timeout: 10000, + }); - // Verify the iDevice is saved and shows the video container - const videoContainer = page.locator('#node-content .interactive-video .exe-interactive-video'); - await expect(videoContainer).toBeAttached({ timeout: 10000 }); + await workarea.save(); }); }); test.describe('Editor Workflow', () => { - // Skip Editor Workflow tests on Firefox - jQuery click handlers in iframes - // don't fire reliably in Firefox, causing the frontpage-block to stay hidden - // after clicking frontpage-link. This is a Firefox-specific browser quirk. + // Skip on Firefox — jQuery click handlers in iframes don't fire reliably in Firefox test.skip( ({ browserName }) => browserName === 'firefox', 'Firefox has issues with jQuery click handlers in iframes', ); - test('should open editor, create cover, and save', async ({ authenticatedPage, createProject }) => { + test('should open editor, create cover, save, and persist after reload', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); const projectUuid = await createProject(page, 'Interactive Video Editor Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add iDevice - await addInteractiveVideoIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'interactive-video'); + await page + .waitForFunction(() => document.querySelector('#interactiveVideoFile') !== null, undefined, { + timeout: 10000, + }) + .catch(() => {}); - // Upload video file await uploadVideoFile(page, TEST_DATA.videoFixture); - // Open the editor - const editorIframe = await openVideoEditor(page); - - // Create a cover - await createCover(page, editorIframe, TEST_DATA.coverTitle, TEST_DATA.coverIntro); - - // Save and close the editor - await saveAndCloseEditor(page, editorIframe); - - // Save the iDevice - await saveInteractiveVideoIdevice(page); - - // Verify the iDevice shows the interactive video content - const videoContainer = page.locator('#node-content .interactive-video .exe-interactive-video'); - await expect(videoContainer).toBeAttached({ timeout: 10000 }); - }); - - test('should persist editor changes after reload', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Interactive Video Persist Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add iDevice and upload video - await addInteractiveVideoIdeviceFromPanel(page); - await uploadVideoFile(page, TEST_DATA.videoFixture); - - // Open editor and create cover + // Open the editor, create cover and text slide const editorIframe = await openVideoEditor(page); await createCover(page, editorIframe, TEST_DATA.coverTitle, TEST_DATA.coverIntro); + await createTextSlide(editorIframe, TEST_DATA.textSlideContent); await saveAndCloseEditor(page, editorIframe); - // Debug: Check what data is in activityToSave after editor closes - const activityData = await page.evaluate(() => { - const data = (window as any).top?.interactiveVideoEditor?.activityToSave; - return data ? JSON.stringify(data) : 'undefined'; - }); - console.log('[DEBUG] activityToSave after editor close:', activityData); - - // Save the iDevice await saveInteractiveVideoIdevice(page); - - // Debug: Check the HTML content of the iDevice after saving - const ideviceHtml = await page.evaluate(() => { - const idevice = document.querySelector('#node-content .interactive-video .exe-interactive-video'); - return idevice?.innerHTML || 'not found'; + await expect(page.locator('#node-content .interactive-video .exe-interactive-video')).toBeAttached({ + timeout: 10000, }); - console.log('[DEBUG] iDevice HTML after save (first 500 chars):', ideviceHtml.substring(0, 500)); - // Save the project + // Save and reload to verify persistence await workarea.save(); - await page.waitForTimeout(500); - - // Reload the page await reloadPage(page); + await selectFirstPage(page); - // Navigate to the page - const pageNode = page - .locator('.nav-element-text') - .filter({ hasText: /New page|Nueva página/i }) - .first(); - if ((await pageNode.count()) > 0) { - await pageNode.click({ force: true, timeout: 5000 }); - await page.waitForTimeout(500); - } - - // Verify the iDevice is still there with the video container + // Verify iDevice and cover content persisted const videoContainer = page.locator('#node-content .interactive-video .exe-interactive-video'); await expect(videoContainer).toBeAttached({ timeout: 15000 }); - - // Debug: Check the HTML content after reload - const ideviceHtmlAfterReload = await page.evaluate(() => { - const idevice = document.querySelector('#node-content .interactive-video .exe-interactive-video'); - return idevice?.innerHTML || 'not found'; - }); - console.log( - '[DEBUG] iDevice HTML after reload (first 800 chars):', - ideviceHtmlAfterReload.substring(0, 800), - ); - - // Debug: Check if there's a script tag with JSON data - const jsonContent = await page.evaluate(() => { - const script = document.querySelector('#exe-interactive-video-contents'); - return script?.textContent || 'script not found'; - }); - console.log('[DEBUG] JSON content after reload:', jsonContent.substring(0, 500)); - - // Verify the cover content is present - const coverContent = page.locator('#node-content .interactive-video .exe-interactive-video'); - await expect(coverContent).toContainText(TEST_DATA.coverTitle, { timeout: 10000 }); + await expect(videoContainer).toContainText(TEST_DATA.coverTitle, { timeout: 10000 }); }); }); test.describe('Preview Panel', () => { - // Skip Preview Panel tests on Firefox - depends on createCover which has Firefox issues + // Skip on Firefox — depends on createCover which has Firefox issues test.skip( ({ browserName }) => browserName === 'firefox', 'Firefox has issues with jQuery click handlers in iframes', @@ -663,38 +381,32 @@ test.describe('Interactive Video iDevice', () => { const projectUuid = await createProject(page, 'Interactive Video Preview Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add iDevice and upload video - await addInteractiveVideoIdeviceFromPanel(page); - await uploadVideoFile(page, TEST_DATA.videoFixture); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'interactive-video'); + await page + .waitForFunction(() => document.querySelector('#interactiveVideoFile') !== null, undefined, { + timeout: 10000, + }) + .catch(() => {}); - // Open editor and create cover with a slide + await uploadVideoFile(page, TEST_DATA.videoFixture); const editorIframe = await openVideoEditor(page); await createCover(page, editorIframe, TEST_DATA.coverTitle, TEST_DATA.coverIntro); await createTextSlide(editorIframe, TEST_DATA.textSlideContent); await saveAndCloseEditor(page, editorIframe); - - // Save the iDevice await saveInteractiveVideoIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const previewIframe = page.frameLocator('#preview-iframe'); - await previewIframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); + await previewIframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Verify the interactive video container is visible in preview - const videoContainer = previewIframe.locator('.exe-interactive-video').first(); - await expect(videoContainer).toBeAttached({ timeout: 10000 }); + await expect(previewIframe.locator('.exe-interactive-video').first()).toBeAttached({ timeout: 10000 }); }); }); @@ -704,10 +416,8 @@ test.describe('Interactive Video iDevice', () => { const projectUuid = await createProject(page, 'Config API Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Verify eXeLearning.config exists and has expected properties const configAPI = await page.evaluate(() => { const config = (window as any).eXeLearning?.config; return { @@ -723,7 +433,6 @@ test.describe('Interactive Video iDevice', () => { expect(configAPI.hasBasePath).toBe(true); expect(configAPI.hasFullURL).toBe(true); - // Verify resolveAssetUrl function is available (via eXeLearningAssetResolver) const hasResolveAssetUrl = await page.evaluate(() => { return typeof (window as any).eXeLearningAssetResolver?.resolve === 'function'; }); diff --git a/test/e2e/playwright/specs/idevices/magnifier.spec.ts b/test/e2e/playwright/specs/idevices/magnifier.spec.ts index f1ebe76f4..50d9422e2 100644 --- a/test/e2e/playwright/specs/idevices/magnifier.spec.ts +++ b/test/e2e/playwright/specs/idevices/magnifier.spec.ts @@ -1,7 +1,12 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { waitForAppReady, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + waitForAppReady, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; -import type { Page } from '@playwright/test'; /** * E2E Tests for Magnifier iDevice @@ -14,95 +19,32 @@ import type { Page } from '@playwright/test'; */ /** - * Helper to add a magnifier iDevice by selecting the page and clicking the magnifier iDevice + * Helper to save the magnifier iDevice and wait for edition mode to end */ -async function addMagnifierIdeviceFromPanel(page: Page): Promise { - // First, select a page in the navigation tree - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - // Wait for the page content area to switch from metadata to page editor - await page.waitForTimeout(500); - - // Wait for node-content to show page content - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => { - // Continue anyway - }); - - // The iDevices menu uses categories with h3 headings - // We need to click on the category heading to expand it - // Magnifier is in "Information and presentation" category - - // Find and click the "Information and presentation" category heading - const categoryHeading = page.locator('#menu_idevices_content h3').filter({ - hasText: /Information|Información/i, - }); - - if ((await categoryHeading.count()) > 0) { - await categoryHeading.first().click(); - await page.waitForTimeout(500); - } - - // Now find the magnifier iDevice in the expanded category - const magnifierIdevice = page.locator('.idevice_item[id="magnifier"], [data-testid="idevice-magnifier"]').first(); - - // Wait for it to be visible after expanding category - await magnifierIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await magnifierIdevice.click(); - - // Wait for iDevice to appear in content area (in edition mode) - await page.locator('#node-content article .idevice_node.magnifier').first().waitFor({ timeout: 15000 }); +async function saveMagnifierIdevice(page: import('@playwright/test').Page): Promise { + const block = page.locator('#node-content article .idevice_node.magnifier').first(); + const saveBtn = block.locator('.btn-save-idevice'); + await saveBtn.waitFor({ state: 'visible', timeout: 10000 }); + await saveBtn.click(); + + await page.waitForFunction( + () => { + const idevice = document.querySelector('#node-content article .idevice_node.magnifier'); + return idevice && idevice.getAttribute('mode') !== 'edition'; + }, + undefined, + { timeout: 30000 }, + ); } /** * Helper to select an image using the file picker in magnifier editor */ -async function selectImageForMagnifier(page: Page, fixturePath: string): Promise { - // The file picker button is generated dynamically next to #mnfFileInput - // Wait for the button to be created - await page.waitForTimeout(500); - - // Find the file picker button - it's created after the input by common_edition.js +async function selectImageForMagnifier(page: import('@playwright/test').Page, fixturePath: string): Promise { const pickFileBtn = page.locator( 'input.exe-pick-any-file[data-filepicker="mnfFileInput"], #mnfFileInput + input[type="button"]', ); - // If button not found by specific selector, try generic button in the magnifier form if ((await pickFileBtn.count()) === 0) { const genericBtn = page.locator( '#magnifierIdeviceForm .exe-pick-any-file, #magnifierIdeviceForm input[type="button"][value*="Select"]', @@ -114,33 +56,31 @@ async function selectImageForMagnifier(page: Page, fixturePath: string): Promise await pickFileBtn.first().click(); } - // Wait for Media Library modal await page.waitForSelector('#modalFileManager[data-open="true"], #modalFileManager.show', { timeout: 10000 }); - // Upload image from fixture const fileInput = page.locator('#modalFileManager .media-library-upload-input'); await fileInput.setInputFiles(fixturePath); - // Wait for the uploaded image to appear in the grid const imageItem = page.locator('#modalFileManager .media-library-item').first(); await expect(imageItem).toBeVisible({ timeout: 15000 }); - - // Click to select the uploaded image await imageItem.click(); - // Wait for sidebar content to show (appears when asset is selected) const sidebarContent = page.locator('#modalFileManager .media-library-sidebar-content'); await expect(sidebarContent).toBeVisible({ timeout: 5000 }); - // Click insert button in Media Library const insertBtn = page.locator('#modalFileManager .media-library-insert-btn'); await expect(insertBtn).toBeVisible({ timeout: 5000 }); await insertBtn.click(); - // Wait for modal to close and input to update - await page.waitForTimeout(500); + await page.waitForFunction( + () => { + const modal = document.querySelector('#modalFileManager'); + return !modal || !modal.classList.contains('show'); + }, + undefined, + { timeout: 10000 }, + ); - // Verify the input was updated const inputValue = await page.locator('#mnfFileInput').inputValue(); if (!inputValue) { throw new Error('Image was not selected - #mnfFileInput is still empty'); @@ -148,106 +88,64 @@ async function selectImageForMagnifier(page: Page, fixturePath: string): Promise } test.describe('Magnifier iDevice', () => { - test.describe('Basic Operations', () => { - test('should add magnifier iDevice to blank document', async ({ authenticatedPage, createProject }) => { + test.describe('Basic Operations and Image Configuration', () => { + test('should add magnifier, select custom image, save, and verify data attributes', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); - // Create a new project - const projectUuid = await createProject(page, 'Magnifier Basic Test'); + const projectUuid = await createProject(page, 'Magnifier Workflow Test'); await gotoWorkarea(page, projectUuid); - - // Wait for app initialization await waitForAppReady(page); - // Add a magnifier iDevice using the panel - await addMagnifierIdeviceFromPanel(page); + // Add magnifier iDevice using centralized helpers + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'magnifier'); - // Verify iDevice was added and is in edition mode + // Verify iDevice was added and form elements are visible const magnifierIdevice = page.locator('#node-content article .idevice_node.magnifier').first(); await expect(magnifierIdevice).toBeVisible({ timeout: 10000 }); - - // Verify the magnifier form elements are visible await expect(page.locator('#mnfFileInput')).toBeVisible({ timeout: 5000 }); await expect(page.locator('#mnfPreviewImage')).toBeVisible({ timeout: 5000 }); - }); - }); - - test.describe('Image Configuration', () => { - test('should select custom image from file picker and display correctly', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Magnifier Custom Image Test'); - await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - - // Add magnifier iDevice - await addMagnifierIdeviceFromPanel(page); + // Verify default image (hood.jpg) + const previewSrc = await page.locator('#mnfPreviewImage').getAttribute('src'); + expect(previewSrc).toContain('hood.jpg'); // Select custom image using file picker await selectImageForMagnifier(page, 'test/fixtures/sample-3.jpg'); - // Verify preview image was updated - const previewImg = page.locator('#mnfPreviewImage'); - await expect(previewImg).toBeVisible({ timeout: 5000 }); - - // Verify the file input has the custom image path with asset:// URL format - // New format is asset://uuid.ext (content-addressable, no filename) + // Verify file input has asset:// URL const fileInputValue = await page.locator('#mnfFileInput').inputValue(); - console.log('File input value:', fileInputValue); - expect(fileInputValue).toBeTruthy(); - // The file input should contain asset:// URL with uuid.ext format expect(fileInputValue.startsWith('asset://')).toBe(true); - // Should have .jpg extension (from original file) expect(fileInputValue).toMatch(/^asset:\/\/[a-f0-9-]+\.jpg$/); // Save the iDevice - const block = page.locator('#node-content article .idevice_node.magnifier').first(); - const saveBtn = block.locator('.btn-save-idevice'); - - // Wait for save button to be visible and clickable (Firefox may need more time) - await saveBtn.waitFor({ state: 'visible', timeout: 10000 }); - await page.waitForTimeout(500); - - await saveBtn.click(); - - // Wait for edition mode to end with longer timeout for Firefox - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.magnifier'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 30000 }, - ); + await saveMagnifierIdevice(page); - // Verify the image container is visible in view mode + // Verify view mode container and image load const viewModeContainer = page.locator( '#node-content article .idevice_node.magnifier .ImageMagnifierIdevice, #node-content article .idevice_node.magnifier .MNF-MainContainer', ); await expect(viewModeContainer.first()).toBeVisible({ timeout: 10000 }); - // Verify an image exists in the magnifier const viewModeImg = viewModeContainer.locator('img').first(); await expect(viewModeImg).toBeVisible({ timeout: 5000 }); - // Wait for image to load - await page.waitForTimeout(500); - - // Debug: log the image src - const imgSrc = await viewModeImg.getAttribute('src'); - console.log('Image src after save:', imgSrc); - - // Verify image loaded correctly (naturalWidth > 0) + // Verify image loaded and has data attributes for magnifier effect const naturalWidth = await viewModeImg.evaluate((el: HTMLImageElement) => el.naturalWidth); - console.log('Image naturalWidth:', naturalWidth); expect(naturalWidth).toBeGreaterThan(0); - // Save project + const hasDataAttributes = await viewModeImg.evaluate((el: HTMLImageElement) => { + return ( + el.hasAttribute('data-magnifysrc') || el.hasAttribute('data-zoom') || el.id.includes('magnifier') + ); + }); + expect(hasDataAttributes).toBe(true); + await workarea.save(); }); @@ -255,125 +153,32 @@ test.describe('Magnifier iDevice', () => { const page = authenticatedPage; const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'Magnifier Image Test'); + const projectUuid = await createProject(page, 'Magnifier Default Image Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add magnifier iDevice - await addMagnifierIdeviceFromPanel(page); - - // The magnifier loads with a default image (hood.jpg) - // Verify preview image is visible in editor - const previewImg = page.locator('#mnfPreviewImage'); - await expect(previewImg).toBeVisible({ timeout: 5000 }); - - // Verify the default image src contains hood.jpg - const previewSrc = await previewImg.getAttribute('src'); - expect(previewSrc).toContain('hood.jpg'); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'magnifier'); - // Save the iDevice (with default image) - const block = page.locator('#node-content article .idevice_node.magnifier').first(); - const saveBtn = block.locator('.btn-save-idevice'); + // Save with default image + await saveMagnifierIdevice(page); - // Wait for save button to be visible and clickable (Firefox may need more time) - await saveBtn.waitFor({ state: 'visible', timeout: 10000 }); - await page.waitForTimeout(500); - - await saveBtn.click(); - - // Wait for edition mode to end with longer timeout for Firefox - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.magnifier'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 30000 }, - ); - - // Verify the image container is visible in view mode const viewModeContainer = page.locator( '#node-content article .idevice_node.magnifier .ImageMagnifierIdevice, #node-content article .idevice_node.magnifier .MNF-MainContainer', ); await expect(viewModeContainer.first()).toBeVisible({ timeout: 10000 }); - // Verify an image exists in the magnifier const viewModeImg = viewModeContainer.locator('img').first(); await expect(viewModeImg).toBeVisible({ timeout: 5000 }); - // Wait for image to load and verify it loaded correctly - await page.waitForTimeout(500); const naturalWidth = await viewModeImg.evaluate((el: HTMLImageElement) => el.naturalWidth); expect(naturalWidth).toBeGreaterThan(0); - // Save project await workarea.save(); }); }); - test.describe('Magnifier Effect', () => { - test('should have magnifier data attributes after save', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'Magnifier Hover Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add magnifier iDevice - await addMagnifierIdeviceFromPanel(page); - - // Save the iDevice with default image - const block = page.locator('#node-content article .idevice_node.magnifier').first(); - const saveBtn = block.locator('.btn-save-idevice'); - - // Wait for save button to be visible and clickable (Firefox may need more time) - await saveBtn.waitFor({ state: 'visible', timeout: 10000 }); - await page.waitForTimeout(500); // Brief wait for any animations - - // Click save button - await saveBtn.click(); - - // Wait for edition mode to end with longer timeout for Firefox - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.magnifier'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 30000 }, - ); - - // Find the magnifier image container - const magnifierContainer = page.locator( - '#node-content article .idevice_node.magnifier .ImageMagnifierIdevice, #node-content article .idevice_node.magnifier .MNF-MainContainer', - ); - await expect(magnifierContainer.first()).toBeVisible({ timeout: 10000 }); - - // Get the image inside the container - const magnifierImg = magnifierContainer.locator('img').first(); - await expect(magnifierImg).toBeVisible({ timeout: 5000 }); - - // Wait for image to load - await page.waitForTimeout(500); - - // Verify the magnifier is set up with proper data attributes - // The image should have data-magnifysrc and data-zoom attributes for the magnifier effect - const hasDataAttributes = await magnifierImg.evaluate((el: HTMLImageElement) => { - return ( - el.hasAttribute('data-magnifysrc') || el.hasAttribute('data-zoom') || el.id.includes('magnifier') - ); - }); - - expect(hasDataAttributes).toBe(true); - - // Verify the image loaded correctly - const naturalWidth = await magnifierImg.evaluate((el: HTMLImageElement) => el.naturalWidth); - expect(naturalWidth).toBeGreaterThan(0); - }); - }); - test.describe('Preview Panel', () => { test('should display correctly in preview panel', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; @@ -381,57 +186,30 @@ test.describe('Magnifier iDevice', () => { const projectUuid = await createProject(page, 'Magnifier Preview Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add magnifier iDevice - await addMagnifierIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'magnifier'); - // Save the iDevice with default image - const block = page.locator('#node-content article .idevice_node.magnifier').first(); - const saveBtn = block.locator('.btn-save-idevice'); + await saveMagnifierIdevice(page); - // Wait for save button to be visible and clickable (Firefox may need more time) - await saveBtn.waitFor({ state: 'visible', timeout: 10000 }); - await page.waitForTimeout(500); - - await saveBtn.click(); - - // Wait for edition mode to end with longer timeout for Firefox - await page.waitForFunction( - () => { - const idevice = document.querySelector('#node-content article .idevice_node.magnifier'); - return idevice && idevice.getAttribute('mode') !== 'edition'; - }, - undefined, - { timeout: 30000 }, - ); - - // Save project await workarea.save(); - await page.waitForTimeout(500); // Open preview panel await page.click('#head-bottom-preview'); const previewPanel = page.locator('#previewsidenav'); await expect(previewPanel).toBeVisible({ timeout: 15000 }); - // Wait for iframe to load const iframe = page.frameLocator('#preview-iframe'); await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Verify magnifier container exists in preview const previewMagnifierContainer = iframe.locator('.MNF-MainContainer, .ImageMagnifierIdevice'); await expect(previewMagnifierContainer.first()).toBeVisible({ timeout: 10000 }); - // Verify image is visible in preview const previewImg = iframe.locator('.ImageMagnifierIdevice img, .MNF-MainContainer img'); await expect(previewImg.first()).toBeVisible({ timeout: 10000 }); - // Wait for image to load - await page.waitForTimeout(500); - - // Verify image loaded correctly (not broken) const naturalWidth = await previewImg.first().evaluate((el: HTMLImageElement) => el.naturalWidth); expect(naturalWidth).toBeGreaterThan(0); }); diff --git a/test/e2e/playwright/specs/idevices/relate.spec.ts b/test/e2e/playwright/specs/idevices/relate.spec.ts index 9f6722f41..5464dec40 100644 --- a/test/e2e/playwright/specs/idevices/relate.spec.ts +++ b/test/e2e/playwright/specs/idevices/relate.spec.ts @@ -1,5 +1,11 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; import type { Page, FrameLocator } from '@playwright/test'; @@ -7,10 +13,10 @@ import type { Page, FrameLocator } from '@playwright/test'; * E2E Tests for Relate iDevice * * Tests the Relate (matching pairs) iDevice functionality including: - * - Basic operations (add pairs, upload images) - * - Multiple pairs (2-3 pairs) + * - Adding pairs with text and images, save * - Preview rendering (canvas dimensions fix) * - Creating connections/arrows between pairs in preview + * - Persistence after reload */ const TEST_FIXTURES = { @@ -18,87 +24,6 @@ const TEST_FIXTURES = { image2: 'test/fixtures/sample-3.jpg', }; -/** - * Helper to select a page in the navigation tree (required before adding iDevices) - */ -async function selectPageNode(page: Page): Promise { - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - await page.waitForTimeout(500); - - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => {}); -} - -/** - * Helper to add a Relate iDevice by expanding the category and clicking the iDevice - */ -async function addRelateIdeviceFromPanel(page: Page): Promise { - await selectPageNode(page); - - // Expand "Interactive activities" category - const interactiveCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Interactive|Interactiv/i }), - }) - .first(); - - if ((await interactiveCategory.count()) > 0) { - const isCollapsed = await interactiveCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - const label = interactiveCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - await page.waitForTimeout(500); - - // Find and click the Relate iDevice - const relateIdevice = page.locator('.idevice_item[id="relate"], [data-testid="idevice-relate"]').first(); - await relateIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await relateIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.relate').first().waitFor({ timeout: 15000 }); -} - /** * Helper to upload an image via the file picker input */ @@ -114,20 +39,16 @@ async function uploadImageViaFilePicker(page: Page, inputSelector: string, fixtu await input.click(); } - // Wait for Media Library modal await page.waitForSelector('#modalFileManager[data-open="true"], #modalFileManager.show', { timeout: 10000 }); - // Upload file const fileInput = page.locator('#modalFileManager .media-library-upload-input'); await fileInput.setInputFiles(fixturePath); - // Wait for item and select it const mediaItem = page.locator('#modalFileManager .media-library-item').first(); await mediaItem.waitFor({ state: 'visible', timeout: 15000 }); await mediaItem.click(); await page.waitForTimeout(500); - // Insert const insertBtn = page.locator( '#modalFileManager .media-library-insert-btn, #modalFileManager button:has-text("Insert"), #modalFileManager button:has-text("Insertar")', ); @@ -155,24 +76,20 @@ async function fillPairData( backText: string, backImagePath: string | null, ): Promise { - // Fill front side text const frontTextInput = page.locator('#rclEText'); await frontTextInput.waitFor({ state: 'visible', timeout: 5000 }); await frontTextInput.clear(); await frontTextInput.fill(frontText); - // Upload front image if provided if (frontImagePath) { await uploadImageViaFilePicker(page, '#rclEURLImage', frontImagePath); } - // Fill back side text const backTextInput = page.locator('#rclETextBack'); await backTextInput.waitFor({ state: 'visible', timeout: 5000 }); await backTextInput.clear(); await backTextInput.fill(backText); - // Upload back image if provided if (backImagePath) { await uploadImageViaFilePicker(page, '#rclEURLImageBack', backImagePath); } @@ -209,15 +126,12 @@ async function saveRelateIdevice(page: Page): Promise { * Helper to verify canvas is properly initialized with correct dimensions */ async function verifyCanvasInitialized(iframe: FrameLocator): Promise { - // Wait for the game container to be visible const gameContainer = iframe.locator('[id^="rlcContainerGame-"]').first(); await gameContainer.waitFor({ state: 'visible', timeout: 15000 }); - // Check canvas has proper dimensions const canvas = iframe.locator('.RLCP-Canvas').first(); await expect(canvas).toBeVisible({ timeout: 10000 }); - // Verify canvas has non-zero dimensions const canvasDimensions = await canvas.evaluate(el => { const rect = el.getBoundingClientRect(); return { width: rect.width, height: rect.height }; @@ -227,81 +141,36 @@ async function verifyCanvasInitialized(iframe: FrameLocator): Promise { expect(canvasDimensions.height).toBeGreaterThan(0); } -/** - * Helper to create a connection/arrow between a word and a definition in preview - */ -async function createConnection(iframe: FrameLocator, wordIndex: number, definitionIndex: number): Promise { - const word = iframe.locator('.RLCP-Word').nth(wordIndex); - const definition = iframe.locator('.RLCP-Definition').nth(definitionIndex); - - await word.waitFor({ state: 'visible', timeout: 10000 }); - await definition.waitFor({ state: 'visible', timeout: 10000 }); - - // Get bounding boxes - const wordBox = await word.boundingBox(); - const defBox = await definition.boundingBox(); - - if (!wordBox || !defBox) { - throw new Error('Could not get bounding boxes for word and definition'); - } - - // Simulate drag from word to definition - const startX = wordBox.x + wordBox.width - 5; - const startY = wordBox.y + wordBox.height / 2; - const endX = defBox.x + 5; - const endY = defBox.y + defBox.height / 2; - - // Get the page from frame - const page = iframe.owner().page(); - - // Perform the drag - await page.mouse.move(startX, startY); - await page.mouse.down(); - await page.mouse.move(endX, endY, { steps: 10 }); - await page.mouse.up(); - - await page.waitForTimeout(500); -} - test.describe('Relate iDevice', () => { - test.describe('Basic Operations', () => { - test('should add relate iDevice to page', async ({ authenticatedPage, createProject }) => { + test.describe('Workflow', () => { + test('should add pairs with images and save', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'Relate Basic Test'); + const projectUuid = await createProject(page, 'Relate Workflow Test'); await gotoWorkarea(page, projectUuid); - // Add a relate iDevice - await addRelateIdeviceFromPanel(page); + // Add relate iDevice using centralized helpers + await selectFirstPage(page); + await expandIdeviceCategory(page, /Interactive|Interactiv/i); + await addIdevice(page, 'relate'); - // Verify iDevice was added and is in edition mode + // Verify iDevice was added and form elements are visible const idevice = page.locator('#node-content article .idevice_node.relate').first(); await expect(idevice).toBeVisible({ timeout: 10000 }); - - // Verify edition form elements are visible await expect(page.locator('#rclEText')).toBeVisible({ timeout: 5000 }); await expect(page.locator('#rclEURLImage')).toBeVisible({ timeout: 5000 }); await expect(page.locator('#rclETextBack')).toBeVisible({ timeout: 5000 }); await expect(page.locator('#rclEURLImageBack')).toBeVisible({ timeout: 5000 }); - }); - test('should add multiple pairs and save', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Relate Multiple Pairs Test'); - await gotoWorkarea(page, projectUuid); - - await addRelateIdeviceFromPanel(page); - - // Fill first pair + // Fill first pair with images await fillPairData(page, 'Cat', TEST_FIXTURES.image1, 'Gato', TEST_FIXTURES.image2); // Add second pair await addNewPair(page); await fillPairData(page, 'Dog', TEST_FIXTURES.image2, 'Perro', TEST_FIXTURES.image1); - // Add third pair + // Add third pair (text only) await addNewPair(page); await fillPairData(page, 'Bird', null, 'Pájaro', null); @@ -309,10 +178,8 @@ test.describe('Relate iDevice', () => { const cardCounter = page.locator('#rclENumCards'); await expect(cardCounter).toHaveText('3', { timeout: 5000 }); - // Save the iDevice + // Save await saveRelateIdevice(page); - - // Verify saved const viewModeIdevice = page.locator('#node-content article .idevice_node.relate .relaciona-IDevice'); await expect(viewModeIdevice).toBeVisible({ timeout: 10000 }); @@ -321,125 +188,58 @@ test.describe('Relate iDevice', () => { }); test.describe('Preview Panel', () => { - test('should render canvas with correct dimensions on preview open', async ({ + test('should display canvas, words/definitions, and images correctly in preview', async ({ authenticatedPage, createProject, }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); - const projectUuid = await createProject(page, 'Relate Canvas Test'); + const projectUuid = await createProject(page, 'Relate Preview Test'); await gotoWorkarea(page, projectUuid); - await addRelateIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Interactive|Interactiv/i); + await addIdevice(page, 'relate'); - // Add pairs with images + // Add pairs with images and text await fillPairData(page, 'Apple', TEST_FIXTURES.image1, 'Manzana', TEST_FIXTURES.image2); await addNewPair(page); await fillPairData(page, 'Orange', TEST_FIXTURES.image2, 'Naranja', TEST_FIXTURES.image1); await saveRelateIdevice(page); await workarea.save(); - await page.waitForTimeout(500); // Open preview panel await page.click('#head-bottom-preview'); const previewPanel = page.locator('#previewsidenav'); await expect(previewPanel).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Wait for game to initialize await page.waitForTimeout(500); - // CRITICAL TEST: Verify canvas has correct dimensions - // This catches the bug where canvas was 0x0 on first load + // CRITICAL TEST: Verify canvas has correct dimensions (fixes 0x0 bug) await verifyCanvasInitialized(iframe); - }); - - test('should display words and definitions for matching', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Relate Display Test'); - await gotoWorkarea(page, projectUuid); - await addRelateIdeviceFromPanel(page); - - // Add pairs with text - await fillPairData(page, 'Hello', null, 'Hola', null); - await addNewPair(page); - await fillPairData(page, 'World', null, 'Mundo', null); - - await saveRelateIdevice(page); - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - - await page.waitForTimeout(500); - - // Verify words container is visible + // Verify words and definitions containers are visible const wordsContainer = iframe.locator('[id^="rlcContainerWords-"]').first(); await expect(wordsContainer).toBeVisible({ timeout: 10000 }); - - // Verify definitions container is visible const definitionsContainer = iframe.locator('[id^="rlcContainerDefinitions-"]').first(); await expect(definitionsContainer).toBeVisible({ timeout: 10000 }); // Verify we have 2 words and 2 definitions - const words = iframe.locator('.RLCP-Word'); - const definitions = iframe.locator('.RLCP-Definition'); + await expect(iframe.locator('.RLCP-Word')).toHaveCount(2, { timeout: 10000 }); + await expect(iframe.locator('.RLCP-Definition')).toHaveCount(2, { timeout: 10000 }); - await expect(words).toHaveCount(2, { timeout: 10000 }); - await expect(definitions).toHaveCount(2, { timeout: 10000 }); - }); - - test('should have images displayed correctly in preview', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Relate Images Test'); - await gotoWorkarea(page, projectUuid); - - await addRelateIdeviceFromPanel(page); - - // Add pairs with images - await fillPairData(page, '', TEST_FIXTURES.image1, '', TEST_FIXTURES.image2); - await addNewPair(page); - await fillPairData(page, '', TEST_FIXTURES.image2, '', TEST_FIXTURES.image1); - - await saveRelateIdevice(page); - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - const iframe = page.frameLocator('#preview-iframe'); - await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - - await page.waitForTimeout(500); - - // Verify images are loaded (have valid src) + // Verify images are loaded with valid src const images = iframe.locator('.RLCP-Image'); const imageCount = await images.count(); expect(imageCount).toBeGreaterThanOrEqual(2); - // Check first image has a valid src (blob URL or relative path) const firstImageSrc = await images.first().getAttribute('src'); expect(firstImageSrc).toBeTruthy(); - // With SW-based preview, assets are served via relative paths (content/resources/...) expect(firstImageSrc).toMatch(/^(blob:|content\/resources\/)/); }); }); @@ -452,16 +252,16 @@ test.describe('Relate iDevice', () => { const projectUuid = await createProject(page, 'Relate Connection Test'); await gotoWorkarea(page, projectUuid); - await addRelateIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Interactive|Interactiv/i); + await addIdevice(page, 'relate'); - // Add pairs await fillPairData(page, 'One', null, 'Uno', null); await addNewPair(page); await fillPairData(page, 'Two', null, 'Dos', null); await saveRelateIdevice(page); await workarea.save(); - await page.waitForTimeout(500); // Open preview await page.click('#head-bottom-preview'); @@ -473,18 +273,14 @@ test.describe('Relate iDevice', () => { await page.waitForTimeout(500); - // Verify game is ready (canvas has dimensions) + // Verify game is ready await verifyCanvasInitialized(iframe); - // Try to create a connection - const words = iframe.locator('.RLCP-Word'); - const definitions = iframe.locator('.RLCP-Definition'); - // Click on first word to select it + const words = iframe.locator('.RLCP-Word'); await words.first().click(); await page.waitForTimeout(300); - // Verify word got selected class const firstWord = words.first(); const isSelected = await firstWord.evaluate(el => el.classList.contains('RLCP-Selected')); expect(isSelected).toBe(true); @@ -499,19 +295,19 @@ test.describe('Relate iDevice', () => { const projectUuid = await createProject(page, 'Relate Persistence Test'); await gotoWorkarea(page, projectUuid); - await addRelateIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Interactive|Interactiv/i); + await addIdevice(page, 'relate'); const uniqueText = `Persistence Test ${Date.now()}`; await fillPairData(page, uniqueText, null, 'Match', null); await saveRelateIdevice(page); await workarea.save(); - await page.waitForTimeout(500); // Reload await reloadPage(page); - - await selectPageNode(page); + await selectFirstPage(page); // Verify iDevice is still there const idevice = page.locator('#node-content article .idevice_node.relate').first(); diff --git a/test/e2e/playwright/specs/idevices/rubric.spec.ts b/test/e2e/playwright/specs/idevices/rubric.spec.ts index e471b58ce..5f3d84c19 100644 --- a/test/e2e/playwright/specs/idevices/rubric.spec.ts +++ b/test/e2e/playwright/specs/idevices/rubric.spec.ts @@ -1,7 +1,13 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { waitForAppReady, reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + waitForAppReady, + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; -import type { Page } from '@playwright/test'; /** * E2E Tests for Rubric iDevice @@ -20,122 +26,30 @@ const TEST_DATA = { weight: '5', }; -/** - * Helper to add a Rubric iDevice by selecting the page and clicking the iDevice - */ -async function addRubricIdeviceFromPanel(page: Page): Promise { - // First, select a page in the navigation tree - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - // Wait for the page content area to switch from metadata to page editor - await page.waitForTimeout(500); - - // Wait for node-content to show page content (not project metadata) - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => { - // Continue anyway - }); - - // Expand "Assessment and tracking" category in iDevices panel - const assessmentCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Assessment|Evaluación/i }), - }) - .first(); - - if ((await assessmentCategory.count()) > 0) { - // Check if category is collapsed (has "off" class) - const isCollapsed = await assessmentCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - // Click on the .label to expand - const label = assessmentCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - // Wait for the category content to be visible - await page.waitForTimeout(500); - - // Find the Rubric iDevice - const rubricIdevice = page.locator('.idevice_item[id="rubric"], [data-testid="idevice-rubric"]').first(); - - // Wait for it to be visible and then click - await rubricIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await rubricIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.rubric').first().waitFor({ timeout: 15000 }); -} - /** * Helper to create a new rubric by clicking the "New rubric" button */ -async function createNewRubric(page: Page): Promise { - // Click the "New rubric" button +async function createNewRubric(page: import('@playwright/test').Page): Promise { const newRubricBtn = page.locator('#ri_CreateNewRubric'); await newRubricBtn.waitFor({ state: 'visible', timeout: 10000 }); await newRubricBtn.click(); - - // Wait for the rubric table editor to appear await page.locator('#ri_Table').waitFor({ state: 'visible', timeout: 10000 }); } /** * Helper to edit rubric content - * - * Cell ID mapping for a 4x4 rubric: - * - #ri_Cell-0 = caption (title) - * - #ri_Cell-1 = empty thead th (top-left corner) - * - #ri_Cell-2 to #ri_Cell-5 = level headers (Level 1-4) - * - #ri_Cell-6 = first criteria TH (row 1) - * - #ri_Cell-7 to #ri_Cell-10 = first row descriptors (TDs with weights) - * - #ri_Cell-7-weight = weight for first descriptor */ -async function editRubricContent(page: Page, title: string, descriptor?: string, weight?: string): Promise { - // Edit the title (caption) - #ri_Cell-0 +async function editRubricContent( + page: import('@playwright/test').Page, + title: string, + descriptor?: string, + weight?: string, +): Promise { const titleInput = page.locator('#ri_Cell-0'); await titleInput.waitFor({ state: 'visible', timeout: 5000 }); await titleInput.clear(); await titleInput.fill(title); - // Optionally edit a descriptor cell (#ri_Cell-7 is first descriptor TD in row 1) if (descriptor) { const descriptorInput = page.locator('#ri_Cell-7'); if ((await descriptorInput.count()) > 0) { @@ -144,7 +58,6 @@ async function editRubricContent(page: Page, title: string, descriptor?: string, } } - // Optionally edit a weight value (#ri_Cell-7-weight is weight for first descriptor) if (weight) { const weightInput = page.locator('#ri_Cell-7-weight'); if ((await weightInput.count()) > 0) { @@ -157,18 +70,15 @@ async function editRubricContent(page: Page, title: string, descriptor?: string, /** * Helper to save the rubric iDevice */ -async function saveRubricIdevice(page: Page): Promise { - // Find and click the Save button +async function saveRubricIdevice(page: import('@playwright/test').Page): Promise { const saveBtn = page .locator('#node-content article .idevice_node.rubric button') .filter({ hasText: /^Save$|^Guardar$/i }) .first(); await saveBtn.click(); - // Wait for edition mode to end (table should be normal, not editable) await page.waitForFunction( () => { - // The editable table has input fields; after save, it should be a normal table const editableInputs = document.querySelectorAll('#ri_Table input'); const normalTable = document.querySelector('#node-content .rubric .exe-table'); return editableInputs.length === 0 && normalTable !== null; @@ -179,117 +89,69 @@ async function saveRubricIdevice(page: Page): Promise { } test.describe('Rubric iDevice', () => { - test.describe('Basic Operations', () => { - test('should add rubric iDevice and create new rubric', async ({ authenticatedPage, createProject }) => { + test.describe('Workflow', () => { + test('should add rubric, create, edit, save, and persist after reload', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); - // Create a new project - const projectUuid = await createProject(page, 'Rubric Add Test'); + const projectUuid = await createProject(page, 'Rubric Workflow Test'); await gotoWorkarea(page, projectUuid); - - // Wait for app initialization await waitForAppReady(page); - // Add a rubric iDevice - await addRubricIdeviceFromPanel(page); + // Add rubric iDevice using centralized helpers + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'rubric'); // Verify iDevice was added const rubricIdevice = page.locator('#node-content article .idevice_node.rubric').first(); await expect(rubricIdevice).toBeVisible({ timeout: 10000 }); - // Create a new rubric + // Create new rubric and verify structure await createNewRubric(page); - - // Verify the rubric table editor appeared with default 4x4 structure const rubricTable = page.locator('#ri_Table'); await expect(rubricTable).toBeVisible({ timeout: 10000 }); - - // Verify it has the expected structure (4 levels in thead + empty th) const theadThs = page.locator('#ri_Table thead th'); await expect(theadThs).toHaveCount(5, { timeout: 5000 }); // 1 empty + 4 levels - - // Verify it has 4 criteria rows const tbodyTrs = page.locator('#ri_Table tbody tr'); await expect(tbodyTrs).toHaveCount(4, { timeout: 5000 }); - }); - test('should edit rubric content and save', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'Rubric Edit Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add a rubric iDevice and create new rubric - await addRubricIdeviceFromPanel(page); - await createNewRubric(page); - - // Edit the rubric content - await editRubricContent(page, TEST_DATA.rubricTitle, TEST_DATA.editedDescriptor, TEST_DATA.weight); + // Edit rubric content + const uniqueTitle = `Persistence Test Rubric ${Date.now()}`; + await editRubricContent(page, uniqueTitle, TEST_DATA.editedDescriptor, TEST_DATA.weight); - // Save the iDevice + // Save the iDevice and verify display await saveRubricIdevice(page); - - // Verify the rubric displays correctly after save - const rubricTable = page.locator('#node-content .rubric .exe-table'); - await expect(rubricTable).toBeVisible({ timeout: 10000 }); - - // Verify the title is displayed in the caption + const savedTable = page.locator('#node-content .rubric .exe-table'); + await expect(savedTable).toBeVisible({ timeout: 10000 }); const caption = page.locator('#node-content .rubric .exe-table caption'); - await expect(caption).toContainText(TEST_DATA.rubricTitle, { timeout: 5000 }); - - // Verify the edited descriptor is visible + await expect(caption).toContainText(uniqueTitle, { timeout: 5000 }); await expect(page.locator('#node-content .rubric')).toContainText(TEST_DATA.editedDescriptor, { timeout: 5000, }); - - // Verify the weight is displayed (format: "text (weight)") await expect(page.locator('#node-content .rubric')).toContainText(`(${TEST_DATA.weight})`, { timeout: 5000, }); - }); - test('should persist rubric after reload', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'Rubric Persistence Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add, create and edit rubric - await addRubricIdeviceFromPanel(page); - await createNewRubric(page); - - const uniqueTitle = `Persistence Test Rubric ${Date.now()}`; - await editRubricContent(page, uniqueTitle); - await saveRubricIdevice(page); - - // Save the project + // Save project and reload to verify persistence await workarea.save(); - await page.waitForTimeout(500); - // Reload the page await reloadPage(page); - // Navigate to the page + // Navigate to the page after reload const pageNode = page .locator('.nav-element-text') .filter({ hasText: /New page|Nueva página/i }) .first(); if ((await pageNode.count()) > 0) { await pageNode.click({ force: true, timeout: 5000 }); - await page.waitForTimeout(500); } - // Verify rubric content persisted await expect(page.locator('#node-content .rubric')).toContainText(uniqueTitle, { timeout: 15000 }); - - // Verify the table structure is intact - const rubricTable = page.locator('#node-content .rubric .exe-table'); - await expect(rubricTable).toBeVisible({ timeout: 10000 }); + await expect(page.locator('#node-content .rubric .exe-table')).toBeVisible({ timeout: 10000 }); }); }); @@ -303,60 +165,49 @@ test.describe('Rubric iDevice', () => { const projectUuid = await createProject(page, 'Rubric Preview Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add, create and edit rubric - await addRubricIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Assessment|Evaluación/i); + await addIdevice(page, 'rubric'); await createNewRubric(page); const previewTitle = `Preview Test Rubric ${Date.now()}`; await editRubricContent(page, previewTitle, 'Preview descriptor', '3'); await saveRubricIdevice(page); - // Save project await workarea.save(); - await page.waitForTimeout(500); // Open preview panel await page.click('#head-bottom-preview'); const previewPanel = page.locator('#previewsidenav'); await expect(previewPanel).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); - - // Wait for page to load await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - // Verify the rubric table is displayed + // Verify rubric table is displayed const rubricTable = iframe.locator('.rubric .exe-table, .idevice_node.rubric .exe-table'); await expect(rubricTable).toBeVisible({ timeout: 10000 }); - // Verify the title/caption is correct const caption = iframe.locator('.rubric .exe-table caption, .idevice_node.rubric .exe-table caption'); await expect(caption).toContainText(previewTitle, { timeout: 5000 }); - // Verify the "Apply" button is present + // Verify Apply button const applyButton = iframe.locator('a.exe-rubrics-print'); await expect(applyButton).toBeVisible({ timeout: 10000 }); await expect(applyButton).toContainText(/Apply|Aplicar/i, { timeout: 5000 }); - // Verify the edited descriptor is visible await expect(iframe.locator('.rubric, .idevice_node.rubric')).toContainText('Preview descriptor', { timeout: 5000, }); - - // Verify the weight is displayed await expect(iframe.locator('.rubric, .idevice_node.rubric')).toContainText('(3)', { timeout: 5000 }); - // Verify the .exe-rubrics-strings list is present (needed for Apply popup i18n) + // Verify i18n strings list const rubricStrings = iframe.locator( '.rubric .exe-rubrics-strings, .idevice_node.rubric .exe-rubrics-strings', ); await expect(rubricStrings).toBeAttached({ timeout: 5000 }); - - // Verify the strings list contains the expected i18n keys await expect(rubricStrings.locator('li.activity')).toBeAttached({ timeout: 2000 }); await expect(rubricStrings.locator('li.name')).toBeAttached({ timeout: 2000 }); await expect(rubricStrings.locator('li.date')).toBeAttached({ timeout: 2000 }); diff --git a/test/e2e/playwright/specs/idevices/text.spec.ts b/test/e2e/playwright/specs/idevices/text.spec.ts index a592e997b..e7e386640 100644 --- a/test/e2e/playwright/specs/idevices/text.spec.ts +++ b/test/e2e/playwright/specs/idevices/text.spec.ts @@ -1,7 +1,12 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { waitForAppReady, reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + waitForAppReady, + reloadPage, + gotoWorkarea, + selectFirstPage, + addTextIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; -import { addTextIdevice } from '../../helpers/workarea-helpers'; /** * E2E Tests for Text iDevice @@ -72,14 +77,7 @@ test.describe('Text iDevice', () => { await reloadPage(page); // Navigate to the page (after reload, project shows metadata by default) - const pageNode = page - .locator('.nav-element-text') - .filter({ hasText: /New page|Nueva página/i }) - .first(); - if ((await pageNode.count()) > 0) { - await pageNode.click({ force: true, timeout: 5000 }); - await page.waitForTimeout(500); - } + await selectFirstPage(page); // Verify content persisted await expect(page.locator('#node-content')).toContainText(uniqueContent, { timeout: 15000 }); @@ -1626,14 +1624,7 @@ test.describe('Text iDevice', () => { await reloadPage(page); // 18. Navigate to the page with the iDevice - const pageNode = page - .locator('.nav-element-text') - .filter({ hasText: /New page|Nueva página/i }) - .first(); - if ((await pageNode.count()) > 0) { - await pageNode.click({ force: true }); - await page.waitForTimeout(500); - } + await selectFirstPage(page); // 19. Verify image is visible AFTER reload const imgAfter = page.locator('#node-content article .idevice_node.text img'); diff --git a/test/e2e/playwright/specs/idevices/udl-content.spec.ts b/test/e2e/playwright/specs/idevices/udl-content.spec.ts index 128b61d92..0cbbf96c1 100644 --- a/test/e2e/playwright/specs/idevices/udl-content.spec.ts +++ b/test/e2e/playwright/specs/idevices/udl-content.spec.ts @@ -1,5 +1,12 @@ import { test, expect } from '../../fixtures/auth.fixture'; -import { waitForAppReady, reloadPage, gotoWorkarea } from '../../helpers/workarea-helpers'; +import { + waitForAppReady, + reloadPage, + gotoWorkarea, + selectFirstPage, + expandIdeviceCategory, + addIdevice, +} from '../../helpers/workarea-helpers'; import { WorkareaPage } from '../../pages/workarea.page'; import type { Page } from '@playwright/test'; @@ -7,134 +14,35 @@ import type { Page } from '@playwright/test'; * E2E Tests for UDL Content (Contenido DUA) iDevice * * Tests the UDL Content iDevice functionality including: - * - Basic operations (add, edit main content, save, persist) - * - UDL types (engagement, representation, expression) - * - Multiple blocks (add, reorder, delete) - * - Alternative content tabs (main, simplified, audio, visual) - * - Character button presentation (LUMEN) + * - Basic operations (add, edit content, UDL type selection, save, persist) + * - Multiple content tabs (main, simplified, audio, visual) + * - Multiple blocks (add, edit) * - Preview rendering and interaction + * - Audio player rendering (with MEJS double-init check) */ const TEST_DATA = { mainContent: 'This is the main content for UDL iDevice', - simplifiedContent: 'Easy to read version of the content', - audioContent: 'Audio description for accessibility', - visualContent: 'Visual aid description with images', buttonText: 'Learn More', - projectTitle: 'UDL Content Test Project', }; /** - * Helper to add a UDL Content iDevice by selecting the page and clicking the iDevice - */ -async function addUdlContentIdeviceFromPanel(page: Page): Promise { - // First, select a page in the navigation tree - const pageNodeSelectors = [ - '.nav-element-text:has-text("New page")', - '.nav-element-text:has-text("Nueva página")', - '[data-testid="nav-node-text"]', - '.structure-tree li .nav-element-text', - ]; - - let pageSelected = false; - for (const selector of pageNodeSelectors) { - const element = page.locator(selector).first(); - if ((await element.count()) > 0) { - try { - await element.click({ force: true, timeout: 5000 }); - pageSelected = true; - break; - } catch { - // Try next selector - } - } - } - - if (!pageSelected) { - const treeItem = page.locator('#menu_structure .structure-tree li').first(); - if ((await treeItem.count()) > 0) { - await treeItem.click({ force: true }); - } - } - - // Wait for the page content area to switch from metadata to page editor - await page.waitForTimeout(500); - - // Wait for node-content to show page content (not project metadata) - await page - .waitForFunction( - () => { - const nodeContent = document.querySelector('#node-content'); - const metadata = document.querySelector('#properties-node-content-form'); - return nodeContent && (!metadata || !metadata.closest('.show')); - }, - undefined, - { timeout: 10000 }, - ) - .catch(() => { - // Continue anyway - }); - - // Expand "Information and presentation" category in iDevices panel - // The category structure: .idevice_category > .label (clickable) > h3.idevice_category_name - // Category has class "off" when collapsed - const infoCategory = page - .locator('.idevice_category') - .filter({ - has: page.locator('h3.idevice_category_name').filter({ hasText: /Information|Información/i }), - }) - .first(); - - if ((await infoCategory.count()) > 0) { - // Check if category is collapsed (has "off" class) - const isCollapsed = await infoCategory.evaluate(el => el.classList.contains('off')); - if (isCollapsed) { - // Click on the .label to expand - const label = infoCategory.locator('.label'); - await label.click(); - await page.waitForTimeout(500); - } - } - - // Wait for the category content to be visible - await page.waitForTimeout(500); - - // Find the UDL Content iDevice - const udlContentIdevice = page - .locator('.idevice_item[id="udl-content"], [data-testid="idevice-udl-content"]') - .first(); - - // Wait for it to be visible and then click - await udlContentIdevice.waitFor({ state: 'visible', timeout: 10000 }); - await udlContentIdevice.click(); - - // Wait for iDevice to appear in content area - await page.locator('#node-content article .idevice_node.udl-content').first().waitFor({ timeout: 15000 }); -} - -/** - * Helper to enter edit mode for a UDL Content iDevice - * Note: UDL Content iDevice enters edit mode automatically when added, - * so this just ensures TinyMCE is loaded + * Helper to wait for TinyMCE to initialize and type content into it */ async function enterEditMode(page: Page): Promise { - // UDL Content iDevice is already in edit mode when added - // Just wait for TinyMCE to be fully loaded await page.locator('.tox-menubar').first().waitFor({ state: 'visible', timeout: 15000 }); } /** - * Helper to save the iDevice and wait for edit mode to end + * Helper to save the UDL Content iDevice and wait for edition mode to end */ async function saveIdevice(page: Page): Promise { - // The iDevice has a Save button in the form header const saveBtn = page .locator('#node-content article .idevice_node.udl-content button') .filter({ hasText: /^Save$/ }) .first(); await saveBtn.click(); - // Wait for edition mode to end (TinyMCE should disappear) await page.waitForFunction( () => { const tinyMce = document.querySelector('#node-content article .idevice_node.udl-content .tox-menubar'); @@ -146,7 +54,7 @@ async function saveIdevice(page: Page): Promise { } /** - * Helper to type content in the TinyMCE editor + * Helper to type content in the active TinyMCE editor */ async function typeInTinyMCE(page: Page, text: string): Promise { await page.waitForFunction( @@ -174,148 +82,80 @@ async function typeInTinyMCE(page: Page, text: string): Promise { } /** - * Helper to select a UDL type + * Helper to select a UDL type (engagement, representation, expression) */ async function selectUdlType(page: Page, type: 'engagement' | 'representation' | 'expression'): Promise { - const typeSelector = page.locator(`#udlContentTypeOptions input[value="${type}"]`); - await typeSelector.click(); + await page.locator(`#udlContentTypeOptions input[value="${type}"]`).click(); } /** * Helper to set button text for a block */ async function setButtonText(page: Page, text: string, blockIndex: number = 0): Promise { - // The button text input is inside a paragraph container const buttonTextInput = page.locator('.udlContentFormBlockButtonTxt input[type="text"]').nth(blockIndex); await buttonTextInput.fill(text); } /** - * Helper to add a new block + * Helper to add a new UDL content block */ async function addBlock(page: Page): Promise { const addBlockBtn = page.locator('#udlContentFormAddBlockWrapper a, #udlContentFormAddBlockWrapper button').first(); await addBlockBtn.click(); - await page.waitForTimeout(500); + await page.waitForTimeout(300); } test.describe('UDL Content iDevice', () => { - test.describe('Basic Operations', () => { - test('should add UDL Content iDevice and edit main content', async ({ authenticatedPage, createProject }) => { + test.describe('Workflow', () => { + test('should add iDevice, configure UDL type and content, and persist after reload', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; + const workarea = new WorkareaPage(page); - // Create a new project - const projectUuid = await createProject(page, 'UDL Content Basic Test'); + const projectUuid = await createProject(page, 'UDL Content Workflow Test'); await gotoWorkarea(page, projectUuid); - - // Wait for app initialization await waitForAppReady(page); - // Add a UDL Content iDevice - await addUdlContentIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'udl-content'); // Verify iDevice was added - const udlIdevice = page.locator('#node-content article .idevice_node.udl-content').first(); - await expect(udlIdevice).toBeVisible({ timeout: 10000 }); - - // Enter edit mode + await expect(page.locator('#node-content article .idevice_node.udl-content').first()).toBeVisible({ + timeout: 10000, + }); await enterEditMode(page); - // Select UDL type - await selectUdlType(page, 'representation'); + // Verify 4 content tabs are present with correct labels + const tabs = page.locator('.udlContentFormTabs a'); + await expect(tabs).toHaveCount(4, { timeout: 10000 }); + await expect(tabs.nth(0)).toContainText(/Main content|Contenido principal/i); + await expect(tabs.nth(1)).toContainText(/Easier to read|Lectura facilitada/i); + await expect(tabs.nth(2)).toContainText(/Audio/i); + await expect(tabs.nth(3)).toContainText(/Visual aid|Apoyo visual/i); + await expect(tabs.nth(0)).toHaveClass(/active/); - // Set button text + // Configure UDL type, button text, and content + await selectUdlType(page, 'representation'); await setButtonText(page, TEST_DATA.buttonText); - - // Type main content - await typeInTinyMCE(page, TEST_DATA.mainContent); - - // Save the iDevice - await saveIdevice(page); - - // Verify content persists in view mode - await page.waitForFunction( - text => { - const content = document.querySelector('#node-content'); - return content && (content.textContent || '').includes(text); - }, - TEST_DATA.mainContent, - { timeout: 15000 }, - ); - - // Verify button text is visible - await expect(page.locator('#node-content')).toContainText(TEST_DATA.buttonText, { timeout: 10000 }); - }); - - test('should save and persist UDL content after reload', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'UDL Content Persistence Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add and edit UDL Content iDevice - await addUdlContentIdeviceFromPanel(page); - await enterEditMode(page); - - // Configure the iDevice - await selectUdlType(page, 'engagement'); const uniqueContent = `Unique UDL content ${Date.now()}`; - await setButtonText(page, 'Click Here'); await typeInTinyMCE(page, uniqueContent); await saveIdevice(page); - // Save the project - await workarea.save(); - await page.waitForTimeout(500); + // Verify representation class is applied and content is visible + await expect(page.locator('.exe-udlContent').first()).toHaveClass(/exe-udlContent-representation/, { + timeout: 10000, + }); + await expect(page.locator('#node-content')).toContainText(TEST_DATA.buttonText, { timeout: 10000 }); - // Reload the page + // Save and reload to verify persistence + await workarea.save(); await reloadPage(page); + await selectFirstPage(page); - // Navigate to the page - const pageNode = page - .locator('.nav-element-text') - .filter({ hasText: /New page|Nueva página/i }) - .first(); - if ((await pageNode.count()) > 0) { - await pageNode.click({ force: true, timeout: 5000 }); - await page.waitForTimeout(500); - } - - // Verify content persisted await expect(page.locator('#node-content')).toContainText(uniqueContent, { timeout: 15000 }); - - // Verify UDL type class is applied - const udlContainer = page.locator('.exe-udlContent').first(); - await expect(udlContainer).toHaveClass(/exe-udlContent-engagement/, { timeout: 10000 }); - }); - }); - - test.describe('UDL Types', () => { - test('should apply correct CSS class for representation type', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'UDL Types Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add a UDL Content iDevice - await addUdlContentIdeviceFromPanel(page); - await enterEditMode(page); - - // Select representation type - await selectUdlType(page, 'representation'); - - // Add minimal content - await typeInTinyMCE(page, 'Content for representation'); - await saveIdevice(page); - - // Verify the correct class is applied - const udlContainer = page.locator('.exe-udlContent').first(); - await expect(udlContainer).toHaveClass(/exe-udlContent-representation/, { timeout: 10000 }); }); }); @@ -325,11 +165,11 @@ test.describe('UDL Content iDevice', () => { const projectUuid = await createProject(page, 'UDL Multiple Blocks Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add UDL Content iDevice - await addUdlContentIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'udl-content'); await enterEditMode(page); // Edit first block @@ -338,14 +178,10 @@ test.describe('UDL Content iDevice', () => { // Add second block await addBlock(page); - - // Wait for second block to appear await page.locator('.udlContentFormBlock').nth(1).waitFor({ state: 'visible', timeout: 10000 }); - - // Edit second block await setButtonText(page, 'Block 2', 1); - // Click on the second block's editor to type in it + // Type in second block's TinyMCE const secondBlockEditor = page .locator('.udlContentFormBlock') .nth(1) @@ -357,96 +193,55 @@ test.describe('UDL Content iDevice', () => { await frame.type('body', 'Content for block 2', { delay: 5 }); } - // Save the iDevice await saveIdevice(page); // Verify both blocks are rendered - const blocks = page.locator('.exe-udlContent-block'); - await expect(blocks).toHaveCount(2, { timeout: 10000 }); - - // Verify content of both blocks + await expect(page.locator('.exe-udlContent-block')).toHaveCount(2, { timeout: 10000 }); await expect(page.locator('#node-content')).toContainText('Block 1', { timeout: 10000 }); await expect(page.locator('#node-content')).toContainText('Block 2', { timeout: 10000 }); }); }); - test.describe('Alternative Content Tabs', () => { - test('should display all 4 content tabs in editor', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - const projectUuid = await createProject(page, 'UDL Alt Content Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add UDL Content iDevice - await addUdlContentIdeviceFromPanel(page); - await enterEditMode(page); - - // Verify all 4 tabs are visible - const tabs = page.locator('.udlContentFormTabs a'); - await expect(tabs).toHaveCount(4, { timeout: 10000 }); - - // Verify tab labels - await expect(tabs.nth(0)).toContainText(/Main content|Contenido principal/i); - await expect(tabs.nth(1)).toContainText(/Easier to read|Lectura facilitada/i); - await expect(tabs.nth(2)).toContainText(/Audio/i); - await expect(tabs.nth(3)).toContainText(/Visual aid|Apoyo visual/i); - - // Verify main tab is active by default - await expect(tabs.nth(0)).toHaveClass(/active/); - }); - }); - - test.describe('Preview', () => { - test('should render correctly in preview', async ({ authenticatedPage, createProject }) => { + test.describe('Preview Panel', () => { + test('should render correctly in preview with engagement type', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; const workarea = new WorkareaPage(page); const projectUuid = await createProject(page, 'UDL Preview Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add and configure UDL Content iDevice - await addUdlContentIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'udl-content'); await enterEditMode(page); await selectUdlType(page, 'engagement'); await setButtonText(page, 'Click to Learn'); await typeInTinyMCE(page, 'This is the main learning content'); - await saveIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const iframe = page.frameLocator('#preview-iframe'); + await iframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Wait for page to load - await iframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Verify UDL container has correct type class - const udlContainer = iframe.locator('.exe-udlContent'); - await expect(udlContainer).toHaveClass(/exe-udlContent-engagement/, { timeout: 10000 }); - - // Verify button is visible with correct text + // Verify engagement class and button text + await expect(iframe.locator('.exe-udlContent')).toHaveClass(/exe-udlContent-engagement/, { + timeout: 10000, + }); const button = iframe.locator('.udl-btn, .udl-character').first(); await expect(button).toBeVisible({ timeout: 10000 }); await expect(button).toContainText('Click to Learn', { timeout: 5000 }); - // Click button to reveal content + // Click button to reveal content and verify await button.click(); - await page.waitForTimeout(500); - - // Verify main content is visible after clicking + await page.waitForTimeout(300); const mainContent = iframe.locator('.exe-udlContent-content-main'); await expect(mainContent).toBeVisible({ timeout: 10000 }); await expect(mainContent).toContainText('main learning content', { timeout: 5000 }); @@ -454,7 +249,7 @@ test.describe('UDL Content iDevice', () => { }); test.describe('Audio Player', () => { - test('should render audio element with type attribute in preview', async ({ + test('should render audio element with correct type and no double MEJS initialization', async ({ authenticatedPage, createProject, }) => { @@ -463,167 +258,66 @@ test.describe('UDL Content iDevice', () => { const projectUuid = await createProject(page, 'UDL Audio Test'); await gotoWorkarea(page, projectUuid); - await waitForAppReady(page); - // Add and configure UDL Content iDevice - await addUdlContentIdeviceFromPanel(page); + await selectFirstPage(page); + await expandIdeviceCategory(page, /Information|Información/i); + await addIdevice(page, 'udl-content'); await enterEditMode(page); + await page.waitForTimeout(300); - // Wait for TinyMCE to be ready - await page.waitForTimeout(500); - - // Insert audio element via TinyMCE API (not directly into iframe body) - // This ensures the content is properly tracked and saved + // Insert audio element via TinyMCE API const audioHtml = - '

Audio content test

'; - + '

Audio test for MEJS

'; await page.evaluate(html => { const editor = (window as any).tinymce?.activeEditor; - if (editor) { - editor.setContent(html); - } + if (editor) editor.setContent(html); }, audioHtml); - // Set button text - await setButtonText(page, 'Listen Here'); - - // Save the iDevice + await setButtonText(page, 'Play Audio'); await saveIdevice(page); - - // Save project await workarea.save(); - await page.waitForTimeout(500); - // Open preview panel await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); + await expect(page.locator('#previewsidenav')).toBeVisible({ timeout: 15000 }); - // Access preview iframe const previewIframe = page.frameLocator('#preview-iframe'); + await previewIframe.locator('article').waitFor({ state: 'attached', timeout: 15000 }); - // Wait for page to load - await previewIframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Click the button to reveal content + // Click button to reveal content const button = previewIframe.locator('.udl-btn, .udl-character').first(); await expect(button).toBeVisible({ timeout: 10000 }); await button.click(); - await page.waitForTimeout(500); + await page.waitForTimeout(300); - // Verify the content is visible + // Verify audio exists (raw audio or MEJS wrapper) const mainContent = previewIframe.locator('.exe-udlContent-content-main'); await expect(mainContent).toBeVisible({ timeout: 10000 }); - // Check that audio exists in the content - // It could be wrapped by MediaElement.js (.mejs-container) or be a raw audio element const audioElement = previewIframe.locator('.exe-udlContent-content-main audio'); const mejsContainer = previewIframe.locator('.exe-udlContent-content-main .mejs-container'); - const audioCount = await audioElement.count(); const mejsCount = await mejsContainer.count(); - - // At least one audio representation should exist expect(audioCount + mejsCount).toBeGreaterThan(0); - // If there's a raw audio element, verify it has the type attribute + // If raw audio, verify type attribute if (audioCount > 0) { const audioType = await audioElement.first().getAttribute('type'); - // Type should be set (either original or detected) if (audioType) { expect(audioType).toContain('audio/'); } } - // If MediaElement.js is active, verify no cannotplay error - if (mejsCount > 0) { - const cannotPlay = previewIframe.locator('.me-cannotplay'); - const cannotPlayCount = await cannotPlay.count(); - expect(cannotPlayCount).toBe(0); - } - }); - - test('should not have double MEJS initialization (mejs-audio and mejs-video classes)', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - const workarea = new WorkareaPage(page); - - const projectUuid = await createProject(page, 'UDL MEJS Double Init Test'); - await gotoWorkarea(page, projectUuid); - - await waitForAppReady(page); - - // Add and configure UDL Content iDevice - await addUdlContentIdeviceFromPanel(page); - await enterEditMode(page); - - // Wait for TinyMCE to be ready - await page.waitForTimeout(500); - - // Insert audio element via TinyMCE API (not directly into iframe body) - // This ensures the content is properly tracked and saved - const audioHtml = - '

Audio test for MEJS

'; - - await page.evaluate(html => { - const editor = (window as any).tinymce?.activeEditor; - if (editor) { - editor.setContent(html); - } - }, audioHtml); - - // Set button text - await setButtonText(page, 'Play Audio'); - - // Save the iDevice - await saveIdevice(page); - - // Save project - await workarea.save(); - await page.waitForTimeout(500); - - // Open preview panel - await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await expect(previewPanel).toBeVisible({ timeout: 15000 }); - - // Access preview iframe - const previewIframe = page.frameLocator('#preview-iframe'); - - // Wait for page to load - await previewIframe.locator('article').waitFor({ state: 'attached', timeout: 10000 }); - - // Click the button to reveal content - const button = previewIframe.locator('.udl-btn, .udl-character').first(); - await expect(button).toBeVisible({ timeout: 10000 }); - await button.click(); - await page.waitForTimeout(500); - - // Check MEJS container classes - const mejsContainer = previewIframe.locator('.exe-udlContent-content-main .mejs-container').first(); - const mejsCount = await mejsContainer.count(); - + // If MEJS, verify no double-init (mejs-audio without mejs-video) if (mejsCount > 0) { - const mejsClass = await mejsContainer.getAttribute('class'); - - // Verify MEJS container does NOT have both mejs-audio and mejs-video classes - // This was a bug where double initialization would add both classes + const mejsClass = await mejsContainer.first().getAttribute('class'); const hasAudioClass = mejsClass?.includes('mejs-audio') ?? false; const hasVideoClass = mejsClass?.includes('mejs-video') ?? false; - - // Should have audio class (since it's an audio element) expect(hasAudioClass).toBe(true); - - // Should NOT have video class (that would indicate double initialization bug) expect(hasVideoClass).toBe(false); - // Note: We do NOT check for me-cannotplay here because data URI audio - // may legitimately fail to play in MEJS. The key verification is that - // we don't have double initialization (both mejs-audio and mejs-video classes). - // Real audio files with proper blob URLs should play correctly. + // Verify no cannotplay error + expect(await previewIframe.locator('.me-cannotplay').count()).toBe(0); } }); }); diff --git a/test/e2e/playwright/specs/theme-import-basic.spec.ts b/test/e2e/playwright/specs/theme-import-basic.spec.ts index a98f42467..b83f24f21 100644 --- a/test/e2e/playwright/specs/theme-import-basic.spec.ts +++ b/test/e2e/playwright/specs/theme-import-basic.spec.ts @@ -1,9 +1,11 @@ import { test, expect, navigateToProject } from '../fixtures/auth.fixture'; +import * as path from 'path'; +import { waitForAppReady, gotoWorkarea, openElpFile } from '../helpers/workarea-helpers'; /** * Basic Theme Tests * - * Tests for theme selection from bundled themes. + * Tests for theme selection from bundled themes and ELP import. * Works in both server and static mode since it doesn't require server APIs. */ @@ -12,50 +14,45 @@ test.describe('Theme Selection - Basic', () => { test('should display bundled themes in styles panel', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; - // Create a project and navigate const projectUuid = await createProject(page, 'Theme Basic Test'); await navigateToProject(page, projectUuid); // Open the Styles panel - const stylesButton = page.locator('#dropdownStyles'); - await expect(stylesButton).toBeVisible(); - await stylesButton.click(); - - // Wait for the styles sidenav to be active + await page.locator('#dropdownStyles').click(); await page.waitForSelector('#stylessidenav.active', { timeout: 5000 }); - // Verify the eXe Styles tab is visible - const exeStylesTab = page.locator('#exestylescontent-tab'); - await expect(exeStylesTab).toBeVisible(); - - // Verify theme cards are displayed + // Verify the eXe Styles tab is visible and theme cards are displayed + await expect(page.locator('#exestylescontent-tab')).toBeVisible(); const themeCards = page.locator('#exestylescontent .theme-card'); - const count = await themeCards.count(); - expect(count).toBeGreaterThan(0); + expect(await themeCards.count()).toBeGreaterThan(0); // Verify 'base' theme is available (default bundled theme) - const baseTheme = page.locator('#exestylescontent .theme-card[data-theme-id="base"]'); - await expect(baseTheme).toBeVisible({ timeout: 5000 }); + await expect(page.locator('#exestylescontent .theme-card[data-theme-id="base"]')).toBeVisible({ + timeout: 5000, + }); }); - test('should select a bundled theme and apply it', async ({ authenticatedPage, createProject }) => { + test('should select a bundled theme, apply it, and verify in ThemesManager', async ({ + authenticatedPage, + createProject, + }) => { const page = authenticatedPage; - // Create a project and navigate const projectUuid = await createProject(page, 'Theme Selection Test'); await navigateToProject(page, projectUuid); - // Open the Styles panel - const stylesButton = page.locator('#dropdownStyles'); - await stylesButton.click(); + // Wait for ThemesManager to be initialized + await page.waitForFunction(() => (window as any).eXeLearning?.app?.themes?.selected, undefined, { + timeout: 30000, + }); + + // Open the Styles panel and click the 'base' theme + await page.locator('#dropdownStyles').click(); await page.waitForSelector('#stylessidenav.active', { timeout: 5000 }); - // Click on the 'base' theme card const baseTheme = page.locator('#exestylescontent .theme-card[data-theme-id="base"]'); await expect(baseTheme).toBeVisible({ timeout: 5000 }); await baseTheme.click(); - - // Wait for theme to be applied await page.waitForTimeout(500); // Verify the theme is selected (has 'selected' class) @@ -70,59 +67,75 @@ test.describe('Theme Selection - Basic', () => { }); expect(selectedTheme).toBe('base'); }); - - test('should have theme in ThemesManager after project load', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - // Create a project and navigate - const projectUuid = await createProject(page, 'Theme Manager Test'); - await navigateToProject(page, projectUuid); - - // Wait for ThemesManager to be initialized - await page.waitForFunction(() => (window as any).eXeLearning?.app?.themes?.selected, undefined, { - timeout: 30000, - }); - - // Get the currently selected theme - const selectedTheme = await page.evaluate(() => { - const themes = (window as any).eXeLearning?.app?.themes; - return { - id: themes?.selected?.id, - name: themes?.selected?.name, - }; - }); - - // Theme should be defined (at least the default 'base' theme) - expect(selectedTheme.id || selectedTheme.name).toBeTruthy(); - }); }); test.describe('Theme Styles Application', () => { test('should apply theme CSS to preview', async ({ authenticatedPage, createProject }) => { const page = authenticatedPage; - // Create a project and navigate const projectUuid = await createProject(page, 'Theme CSS Test'); await navigateToProject(page, projectUuid); // Open preview await page.click('#head-bottom-preview'); - const previewPanel = page.locator('#previewsidenav'); - await previewPanel.waitFor({ state: 'visible', timeout: 15000 }); + await page.locator('#previewsidenav').waitFor({ state: 'visible', timeout: 15000 }); // Wait for preview iframe to load const previewIframe = page.frameLocator('#preview-iframe'); await previewIframe.locator('body').waitFor({ timeout: 15000 }); - // Verify theme CSS is applied (check for theme-specific class or style) + // Verify theme CSS is applied (check for theme-specific class or stylesheet) const themeClass = await previewIframe.locator('body').evaluate(el => { - // Check for theme-related classes or stylesheet const hasThemeClass = el.classList.contains('exe-themeBase') || el.className.includes('theme'); const hasStylesheet = Array.from(document.styleSheets).some(sheet => sheet.href?.includes('theme')); return hasThemeClass || hasStylesheet || true; // At minimum, body should exist }); - expect(themeClass).toBeTruthy(); }); }); + + test.describe('Theme Import from ELPX', () => { + test('should show correct theme in styles panel and ThemesManager after opening elpx file', async ({ + authenticatedPage, + createProject, + }) => { + const page = authenticatedPage; + + const projectUuid = await createProject(page, 'Theme ELPX Import Test'); + await gotoWorkarea(page, projectUuid); + await waitForAppReady(page); + + // Import the fixture ELPX file with a known theme ('base') + const fixturePath = path.resolve( + __dirname, + '../../../fixtures/un-contenido-de-ejemplo-para-probar-estilos-y-catalogacion.elpx', + ); + await openElpFile(page, fixturePath, 1); + + // Wait for theme to be applied + await page.waitForFunction(() => (window as any).eXeLearning?.app?.themes?.selected, undefined, { + timeout: 30000, + }); + + // Verify ThemesManager has the theme set + const selectedTheme = await page.evaluate(() => { + return ( + (window as any).eXeLearning?.app?.themes?.selected?.id || + (window as any).eXeLearning?.app?.themes?.selected?.name + ); + }); + expect(selectedTheme).toBeDefined(); + + // Open the Styles panel and verify the selected theme is shown + await page.locator('#dropdownStyles').click(); + await page.waitForSelector('#stylessidenav.active', { timeout: 5000 }); + + const selectedCards = page.locator('#exestylescontent .theme-card.selected'); + await expect(selectedCards).toHaveCount(1); + + // The selected theme should match what ThemesManager reports + const selectedThemeId = await selectedCards.getAttribute('data-theme-id'); + expect(selectedThemeId).toBe(selectedTheme); + }); + }); }); diff --git a/test/e2e/playwright/specs/theme-selection.spec.ts b/test/e2e/playwright/specs/theme-selection.spec.ts deleted file mode 100644 index 159fd7fc0..000000000 --- a/test/e2e/playwright/specs/theme-selection.spec.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { test, expect } from '../fixtures/auth.fixture'; -import * as path from 'path'; -import { waitForAppReady, gotoWorkarea } from '../helpers/workarea-helpers'; - -test.describe('Theme Selection on ELP Import', () => { - /** - * Test that theme from imported .elpx is correctly reflected in the styles panel UI - * This ensures the fix for the bug where theme was applied but UI wasn't updated - */ - test('should show correct theme selected in styles panel after opening elpx file', async ({ - authenticatedPage, - createProject, - }) => { - const page = authenticatedPage; - - // 1. Create a new project first - const projectUuid = await createProject(page, 'Theme Test Project'); - expect(projectUuid).toBeDefined(); - - // 2. Navigate to the project - await gotoWorkarea(page, projectUuid); - - // Wait for the app to fully initialize - await waitForAppReady(page); - - // 3. Import a fixture .elpx file with a known theme ('base') - const fixturePath = path.resolve( - __dirname, - '../../../fixtures/un-contenido-de-ejemplo-para-probar-estilos-y-catalogacion.elpx', - ); - - // Use the import API or file input to load the fixture - // First, try to find File > Import menu - const fileMenu = page.locator('#navbarFile, [data-menu="file"], .navbar-file'); - - if ((await fileMenu.count()) > 0) { - await fileMenu.click(); - - // Wait for dropdown to appear - await page.waitForTimeout(300); - - // Look for import option - const importOption = page.locator( - '[data-action="import-local-ode-file"], .import-ode-file, li:has-text("Importar")', - ); - - if ((await importOption.count()) > 0) { - // Setup file chooser before clicking - const fileChooserPromise = page.waitForEvent('filechooser'); - await importOption.click(); - const fileChooser = await fileChooserPromise; - await fileChooser.setFiles(fixturePath); - - // Wait for import to complete - await page.waitForFunction( - () => { - const nav = document.querySelector('#structure-menu-nav'); - return nav && nav.querySelectorAll('.page-node').length > 0; - }, - undefined, - { timeout: 30000 }, - ); - } - } - - // 4. Wait a moment for theme to be applied - await page.waitForTimeout(500); - - // 5. Open the Styles panel - const stylesButton = page.locator('#dropdownStyles'); - await expect(stylesButton).toBeVisible(); - await stylesButton.click(); - - // Wait for the styles sidenav to be active - await page.waitForSelector('#stylessidenav.active', { timeout: 5000 }); - - // 6. Verify the 'base' theme is shown as selected - // The theme card should have the 'selected' class - const _baseThemeCard = page.locator( - '#exestylescontent .theme-card[data-theme-id="base"], #exestylescontent .theme-card.selected', - ); - - // Check if the selected theme card exists - const selectedCards = page.locator('#exestylescontent .theme-card.selected'); - await expect(selectedCards).toHaveCount(1); - - // Verify it's the base theme that's selected - const selectedThemeId = await selectedCards.getAttribute('data-theme-id'); - expect(selectedThemeId).toBe('base'); - }); - - /** - * Test that ThemesManager.selected is correctly set after import - */ - test('should have correct theme in ThemesManager after import', async ({ authenticatedPage, createProject }) => { - const page = authenticatedPage; - - // Create a project - const projectUuid = await createProject(page, 'Theme Check Project'); - await gotoWorkarea(page, projectUuid); - - // Wait for app initialization - await page.waitForFunction( - () => { - return (window as any).eXeLearning?.app?.themes?.selected; - }, - undefined, - { timeout: 30000 }, - ); - - // Get the currently selected theme from the app - const selectedTheme = await page.evaluate(() => { - return ( - (window as any).eXeLearning.app.themes.selected?.id || - (window as any).eXeLearning.app.themes.selected?.name - ); - }); - - // The theme should be defined (either default or from fixture) - expect(selectedTheme).toBeDefined(); - }); -});