const delay = require('./wait') const { TEST_HOST, TEST_PASS, TEST_USER, TEST_PORT } = require('./env') const { expect } = require('./expect') const log = require('./log') /** * Common file and folder operations for electerm SFTP tests */ /** * Creates a new file in the specified type of file list (local/remote) * Always uses the parent-file-item which is guaranteed to be present * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} fileName - The name of the file to create */ async function createFile (client, type, fileName) { // Always use the parent-file-item for right-click context menu await client.rightClick(`.session-current .file-list.${type} .parent-file-item`, 10, 10) await delay(500) await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("New File")') await delay(400) await client.setValue('.session-current .sftp-item input', fileName) await client.click('.session-current .sftp-panel-title') await delay(3500) // Ensure file creation completes } /** * Creates a new folder in the specified type of file list (local/remote) * Always uses the parent-file-item which is guaranteed to be present * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} folderName - The name of the folder to create */ async function createFolder (client, type, folderName) { await delay(500) // Always use the parent-file-item for right-click context menu await client.rightClick(`.session-current .file-list.${type} .parent-file-item`, 10, 10) await delay(500) await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("New Folder")') await delay(400) await client.setValue('.session-current .sftp-item input', folderName) await client.click('.session-current .sftp-panel-title') await delay(3500) // Ensure folder creation completes } /** * Deletes an item (file or folder) from the specified type of file list * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} itemName - The name of the item to delete */ async function deleteItem (client, type, itemName) { await client.click(`.session-current .file-list.${type} .sftp-item[title="${itemName}"]`) await delay(400) await client.keyboard.press('Delete') await delay(400) await client.keyboard.press('Enter') await delay(2000) } /** * Copies an item using the context menu * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} itemName - The name of the item to copy */ async function copyItem (client, type, itemName) { await client.rightClick(`.session-current .file-list.${type} .sftp-item[title="${itemName}"]`, 10, 10) await delay(1000) // Increased delay for context menu await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Copy")') await delay(1500) // Ensure copy operation registers } /** * Copies an item using keyboard shortcuts * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} itemName - The name of the item to copy */ async function copyItemWithKeyboard (client, type, itemName) { await client.click(`.session-current .file-list.${type} .sftp-item[title="${itemName}"]`) await delay(400) // Use Meta+C on Mac, Ctrl+C otherwise const isMac = process.platform === 'darwin' const modKey = isMac ? 'Meta' : 'Control' await client.keyboard.press(`${modKey}+c`) await delay(1500) // Ensure copy operation registers } /** * Cuts an item using the context menu * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} itemName - The name of the item to cut */ async function cutItem (client, type, itemName) { await client.rightClick(`.session-current .file-list.${type} .sftp-item[title="${itemName}"]`, 10, 10) await delay(800) await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Cut")') await delay(1000) } /** * Pastes an item using the context menu * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') */ async function pasteItem (client, type) { const parentFolderSelector = `.session-current .file-list.${type} .parent-file-item` const realFileSelector = `.session-current .file-list.${type} .real-file-item` // Click elsewhere to ensure the previous context menu is closed await client.click('.session-current .sftp-panel-title') await delay(1000) // Increased delay // Try to right click on the parent file item first (for empty folders) if (await client.locator(parentFolderSelector).count() > 0) { await client.rightClick(parentFolderSelector, 10, 10) } else { // Fall back to real file item if parent item doesn't exist await client.rightClick(realFileSelector, 10, 10) } await delay(1000) // Wait for paste menu to be visible and enabled const pasteMenuItem = await client.locator('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Paste"):not(.ant-dropdown-menu-item-disabled)') await pasteMenuItem.waitFor({ state: 'visible', timeout: 5000 }) await pasteMenuItem.click() await delay(4000) // Increased delay for paste operation } /** * Pastes an item using keyboard shortcuts * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') */ async function pasteItemWithKeyboard (client, type) { // Click on empty space in the file list to ensure focus await client.click(`.session-current .file-list.${type}`) await delay(1000) // Use Meta+V on Mac, Ctrl+V otherwise const isMac = process.platform === 'darwin' const modKey = isMac ? 'Meta' : 'Control' await client.keyboard.press(`${modKey}+v`) await delay(4000) // Increased delay for paste operation } /** * Renames an item using the context menu * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} oldName - The current name of the item * @param {string} newName - The new name for the item */ async function renameItem (client, type, oldName, newName) { await client.rightClick(`.session-current .file-list.${type} .sftp-item[title="${oldName}"]`, 10, 10) await delay(500) await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Rename")') await delay(400) await client.setValue('.session-current .sftp-item input', newName) await client.click('.session-current .sftp-panel-title') await delay(2500) } /** * Enters a folder in the file list * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} folderName - The name of the folder to enter */ async function enterFolder (client, type, folderName) { await client.rightClick(`.session-current .file-list.${type} .sftp-item[title="${folderName}"]`, 10, 10) await delay(800) await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Enter")') await delay(3500) // Increased delay for folder navigation } /** * Navigates to the parent folder * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') */ async function navigateToParentFolder (client, type) { await client.doubleClick(`.session-current .file-list.${type} .parent-file-item`) await delay(3000) } /** * Selects all items in a file list using context menu * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') */ async function selectAllContextMenu (client, type) { await client.rightClick(`.session-current .file-list.${type} .real-file-item`, 10, 10) await delay(500) await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Select All")') await delay(1000) } /** * Accesses folder from the terminal through context menu * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} folderName - The name of the folder to access */ async function accessFolderFromTerminal (client, type, folderName) { await client.rightClick(`.file-list.${type} .sftp-item[title="${folderName}"]`, 10, 10) await delay(500) await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Access this folder from the terminal")') await delay(1000) } async function openNewConnectionForm (client) { await client.click('.btns .anticon-plus-circle') await delay(500) } async function confirmSshHostKeyVerificationIfNeeded (client, timeout = 4000) { const trustButton = client.locator('.custom-modal-wrap button:has-text("Trust and Save")').first() try { await trustButton.waitFor({ state: 'visible', timeout }) } catch { return false } await trustButton.click() await delay(500) return true } /** * Sets up SSH connection for testing (fills form and submits, no SFTP tab) * * @param {Object} client - The Playwright client */ async function setupSshConnection (client, options = {}) { const { host = TEST_HOST, username = TEST_USER, password = TEST_PASS, port = TEST_PORT, openForm = true, waitAfterConnect = 2000, hostKeyModalTimeout = 4000 } = options if (openForm) { await openNewConnectionForm(client) } await client.setValue('#ssh-form_host', host) await client.setValue('#ssh-form_username', username) await client.setValue('#ssh-form_password', password) await client.setValue('#ssh-form_port', port) await client.click('.setting-wrap .ant-btn-primary') await confirmSshHostKeyVerificationIfNeeded(client, hostKeyModalTimeout) await delay(waitAfterConnect) } /** * Sets up SFTP connection for testing (SSH form + SFTP tab) * * @param {Object} client - The Playwright client */ async function setupSftpConnection (client) { await setupSshConnection(client) // Click sftp tab await client.click('.session-current .term-sftp-tabs .type-tab', 1) await delay(2500) } /** * Verify that a file exists in the file list * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} itemName - The name of the item to verify * @returns {Promise} - Whether the file exists */ async function verifyFileExists (client, type, itemName) { const fileItems = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${itemName}"]`) const count = await fileItems.count() return count > 0 } /** * Verify that a file does not exist in the file list * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} itemName - The name of the item to verify * @returns {Promise} - Whether the file does not exist */ async function verifyFileNotExists (client, type, itemName) { return !(await verifyFileExists(client, type, itemName)) } // Selection operations async function selectItemsWithShift (client, type, startIndex, endIndex) { const items = await client.locator(`.session-current .file-list.${type} .real-file-item`) // Click first item await items.nth(startIndex).click() await delay(500) // Shift+click second item await items.nth(endIndex).click({ modifiers: ['Shift'] }) await delay(500) } async function selectItemsWithCtrlOrCmd (client, type, indices) { const items = await client.locator(`.session-current .file-list.${type} .real-file-item`) // Click first item await items.nth(indices[0]).click() await delay(500) // Add remaining items with Cmd/Ctrl for (let i = 1; i < indices.length; i++) { await items.nth(indices[i]).click({ modifiers: process.platform === 'darwin' ? ['Meta'] : ['Control'] }) await delay(500) } } /** * Verifies the current path in the file list input * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} expectedPath - The expected path or part of it * @returns {Promise} - Whether the path matches */ async function verifyCurrentPath (client, type, expectedPath) { const currentPath = await client.getValue(`.session-current .sftp-${type}-section .sftp-title input`) return currentPath.endsWith(expectedPath) } /** * Clicks on the SFTP tab * * @param {Object} client - The Playwright client */ async function clickSftpTab (client) { await client.click('.session-current .term-sftp-tabs .type-tab', 1) await delay(3500) } /** * Counts items in the file list * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {string} selector - The CSS selector for the items (e.g., '.sftp-item' or '.parent-file-item') */ async function countFileListItems (client, type, selector) { const items = await client.locator(`.session-current .file-list.${type} ${selector}`) return await items.count() } /** * Verifies the number of selected items * * @param {Object} client - The Playwright client * @param {string} type - The type of file list ('local' or 'remote') * @param {number} expectedCount - The expected number of selected items */ async function verifySelectionCount (client, type, expectedCount) { const selectedItems = await client.locator(`.session-current .file-list.${type} .sftp-item.selected`) const count = await selectedItems.count() expect(count).toBe(expectedCount, `Expected ${expectedCount} items to be selected, found ${count}`) } /** * Verifies that the fileTransfers array in window.store is empty * Polls with retries to handle timing variations in transfer completion * * @param {Object} client - The Playwright client * @param {number} [timeout=30000] - Max wait time in ms * @param {number} [interval=1000] - Poll interval in ms */ async function verifyFileTransfersComplete (client, timeout = 30000, interval = 1000) { const start = Date.now() let isEmpty = false while (Date.now() - start < timeout) { isEmpty = await client.evaluate(() => { return window.store.fileTransfers.length === 0 }) if (isEmpty) { break } await delay(interval) } expect(isEmpty).toBe(true, `Expected fileTransfers array to be empty after operations complete (waited ${timeout}ms)`) } async function closeApp (electronApp, fileName) { try { await Promise.race([ electronApp.close(), new Promise((resolve, reject) => setTimeout(() => reject(new Error('close timeout')), 5000)) ]) } catch (e) { if (e.message === 'close timeout') { log(`${fileName}: close timed out, killing process`) electronApp.process().kill() } else { console.log(e) } } } module.exports = { createFile, createFolder, deleteItem, copyItem, copyItemWithKeyboard, cutItem, pasteItem, pasteItemWithKeyboard, renameItem, enterFolder, navigateToParentFolder, selectAllContextMenu, accessFolderFromTerminal, setupSshConnection, setupSftpConnection, verifyFileExists, verifyFileNotExists, selectItemsWithShift, selectItemsWithCtrlOrCmd, verifyCurrentPath, clickSftpTab, countFileListItems, verifySelectionCount, verifyFileTransfersComplete, closeApp }