Files
2026-07-13 12:46:20 +08:00

466 lines
16 KiB
JavaScript

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<boolean>} - 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<boolean>} - 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<boolean>} - 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
}