chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const port = 43434
|
||||
|
||||
// Middleware for JSON parsing
|
||||
app.use(express.json())
|
||||
|
||||
// Middleware to check Bearer token
|
||||
app.use((req, res, next) => {
|
||||
const authHeader = req.headers.authorization
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'Missing or invalid authorization header' })
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
// Helper function to extract user input from message
|
||||
function extractUserInput (message) {
|
||||
const match = message.match(/user input: "([^"]*)"/)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
// Generate 5 fake commands that start with user input
|
||||
function generateTestCommands (userInput) {
|
||||
return [
|
||||
`${userInput}-test-command-1`,
|
||||
`${userInput}-test-command-2`,
|
||||
`${userInput}-test-command-3`,
|
||||
`${userInput}-test-command-4`,
|
||||
`${userInput}-test-command-5`
|
||||
]
|
||||
}
|
||||
|
||||
// Generate a test bookmark based on description
|
||||
function generateTestBookmark (description) {
|
||||
return {
|
||||
title: 'Test Server',
|
||||
host: 'test.example.com',
|
||||
port: 22,
|
||||
username: 'testuser',
|
||||
type: 'ssh',
|
||||
description
|
||||
}
|
||||
}
|
||||
|
||||
// Chat completions endpoint
|
||||
app.post('/chat/completions', (req, res) => {
|
||||
const { messages, stream } = req.body
|
||||
const lastMessage = messages[messages.length - 1].content
|
||||
|
||||
if (lastMessage.includes('give me max 5 command suggestions for user input')) {
|
||||
const userInput = extractUserInput(lastMessage)
|
||||
const mockSuggestions = generateTestCommands(userInput)
|
||||
|
||||
// Command suggestions are always non-streaming
|
||||
res.json({
|
||||
choices: [{
|
||||
message: {
|
||||
content: JSON.stringify(mockSuggestions)
|
||||
}
|
||||
}]
|
||||
})
|
||||
} else if (lastMessage.includes('Generate the bookmark JSON')) {
|
||||
const bookmarkData = generateTestBookmark(lastMessage)
|
||||
res.json({
|
||||
choices: [{
|
||||
message: {
|
||||
content: JSON.stringify(bookmarkData, null, 2)
|
||||
}
|
||||
}]
|
||||
})
|
||||
} else if (stream) {
|
||||
// Handle streaming response
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/plain',
|
||||
'Transfer-Encoding': 'chunked'
|
||||
})
|
||||
|
||||
const mockResponse = '# Response to your query\n\n' +
|
||||
'Here is a sample response with different markdown elements:\n\n' +
|
||||
'## Code Example\n' +
|
||||
'```javascript\n' +
|
||||
'console.log("Hello World!");\n' +
|
||||
'```\n\n' +
|
||||
'## List Example\n' +
|
||||
'- Item 1\n' +
|
||||
'- Item 2\n' +
|
||||
'- Item 3\n\n' +
|
||||
'## Text Formatting\n' +
|
||||
'**Bold text** and *italic text*\n\n' +
|
||||
'> This is a blockquote\n\n' +
|
||||
'[This is a link](https://example.com)'
|
||||
|
||||
// Split response into chunks and send as streaming data
|
||||
const chunks = mockResponse.split(' ')
|
||||
let index = 0
|
||||
|
||||
const sendChunk = () => {
|
||||
if (index < chunks.length) {
|
||||
const content = chunks[index] + (index < chunks.length - 1 ? ' ' : '')
|
||||
const streamData = {
|
||||
choices: [{
|
||||
delta: {
|
||||
content
|
||||
}
|
||||
}]
|
||||
}
|
||||
res.write(`data: ${JSON.stringify(streamData)}\n\n`)
|
||||
index++
|
||||
setTimeout(sendChunk, 50) // Simulate streaming delay
|
||||
} else {
|
||||
res.write('data: [DONE]\n\n')
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
|
||||
sendChunk()
|
||||
} else {
|
||||
// Handle non-streaming response
|
||||
const mockResponse = '# Response to your query\n\n' +
|
||||
'Here is a sample response with different markdown elements:\n\n' +
|
||||
'## Code Example\n' +
|
||||
'```javascript\n' +
|
||||
'console.log("Hello World!");\n' +
|
||||
'```\n\n' +
|
||||
'## List Example\n' +
|
||||
'- Item 1\n' +
|
||||
'- Item 2\n' +
|
||||
'- Item 3\n\n' +
|
||||
'## Text Formatting\n' +
|
||||
'**Bold text** and *italic text*\n\n' +
|
||||
'> This is a blockquote\n\n' +
|
||||
'[This is a link](https://example.com)'
|
||||
|
||||
res.json({
|
||||
choices: [{
|
||||
message: {
|
||||
content: mockResponse
|
||||
}
|
||||
}]
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(port, '127.0.0.1', () => {
|
||||
console.log(`Mock AI API server running at http://127.0.0.1:${port}`)
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
const { resolve } = require('path')
|
||||
const cwd = process.cwd()
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_TEST: 'yes'
|
||||
},
|
||||
args: [
|
||||
resolve(cwd, 'work/app'),
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage'
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
const delay = require('./wait')
|
||||
const { expect } = require('./expect')
|
||||
|
||||
exports.basicTerminalTest = async (client, cmd) => {
|
||||
async function focus () {
|
||||
client.click('.session-current .term-wrap')
|
||||
}
|
||||
async function selectAll () {
|
||||
await client.keyboard.press('Meta+A')
|
||||
await delay(401)
|
||||
}
|
||||
async function copy () {
|
||||
await selectAll()
|
||||
await client.keyboard.press('Meta+C')
|
||||
await delay(401)
|
||||
}
|
||||
await copy()
|
||||
const text1 = await client.readClipboard()
|
||||
await delay(301)
|
||||
await focus()
|
||||
await delay(1010)
|
||||
await client.keyboard.type(cmd)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(1011)
|
||||
await copy()
|
||||
await delay(101)
|
||||
const text2 = await client.readClipboard()
|
||||
expect(text1.trim().length).lessThan(text2.trim().length)
|
||||
}
|
||||
|
||||
exports.getTerminalContent = async function (client) {
|
||||
await client.click('.session-current .term-wrap')
|
||||
await delay(300)
|
||||
await client.keyboard.press('Meta+A')
|
||||
await delay(300)
|
||||
await client.keyboard.press('Meta+C')
|
||||
await delay(300)
|
||||
const clipboardText = await client.readClipboard()
|
||||
await client.keyboard.press('Escape')
|
||||
await delay(300)
|
||||
return clipboardText
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Standard batch-op example workflow for e2e tests.
|
||||
* Covers: connect, command (create file + log), sftp_download,
|
||||
* sftp_upload, command (verify + cleanup).
|
||||
*/
|
||||
|
||||
const {
|
||||
TEST_HOST,
|
||||
TEST_PORT,
|
||||
TEST_USER,
|
||||
TEST_PASS
|
||||
} = require('./env')
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
name: 'Connect SSH',
|
||||
action: 'connect',
|
||||
params: {
|
||||
host: TEST_HOST,
|
||||
port: parseInt(TEST_PORT, 10),
|
||||
username: TEST_USER,
|
||||
authType: 'password',
|
||||
password: TEST_PASS,
|
||||
enableSftp: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Create 5M Test File',
|
||||
action: 'command',
|
||||
prevDelay: 500,
|
||||
afterDelay: 500,
|
||||
command: "fallocate -l 5M /tmp/test_5m_file.bin && rm -f /tmp/test_log.log && echo '[LOG] Created 5M test file at $(date)' >> /tmp/test_log.log"
|
||||
},
|
||||
{
|
||||
name: 'Log creation',
|
||||
action: 'command',
|
||||
afterDelay: 200,
|
||||
command: "ls -la /tmp/test_5m_file.bin >> /tmp/test_log.log 2>&1 && echo '[LOG] File size logged at $(date)' >> /tmp/test_log.log"
|
||||
},
|
||||
{
|
||||
name: 'Download 5M File',
|
||||
action: 'sftp_download',
|
||||
afterDelay: 200,
|
||||
remotePath: '/tmp/test_5m_file.bin',
|
||||
localPath: '/tmp/test_5m_file.bin'
|
||||
},
|
||||
{
|
||||
name: 'Log after download',
|
||||
action: 'command',
|
||||
afterDelay: 200,
|
||||
command: "echo '[LOG] Download complete at $(date)' >> /tmp/test_log.log"
|
||||
},
|
||||
{
|
||||
name: 'Delete Remote 5M File',
|
||||
action: 'command',
|
||||
afterDelay: 200,
|
||||
command: "rm /tmp/test_5m_file.bin && echo '[LOG] Deleted remote 5M file at $(date)' >> /tmp/test_log.log"
|
||||
},
|
||||
{
|
||||
name: 'Upload Downloaded File to Remote',
|
||||
action: 'sftp_upload',
|
||||
afterDelay: 200,
|
||||
localPath: '/tmp/test_5m_file.bin',
|
||||
remotePath: '/tmp/test_5m_file_uploaded.bin'
|
||||
},
|
||||
{
|
||||
name: 'Log after upload',
|
||||
action: 'command',
|
||||
afterDelay: 200,
|
||||
command: "echo '[LOG] Upload complete at $(date)' >> /tmp/test_log.log"
|
||||
},
|
||||
{
|
||||
name: 'Verify and clean up',
|
||||
action: 'command',
|
||||
command: "ls -la /tmp/test_5m_file_uploaded.bin >> /tmp/test_log.log 2>&1 && rm -f /tmp/test_5m_file*.bin && echo '[LOG] Cleaned up at $(date)' >> /tmp/test_log.log"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* extend client functions
|
||||
*/
|
||||
|
||||
const {
|
||||
expect
|
||||
} = require('@playwright/test')
|
||||
const delay = require('./wait')
|
||||
|
||||
module.exports = (client, app) => {
|
||||
client.element = (sel) => {
|
||||
return client.locator(sel).nth(0)
|
||||
}
|
||||
client.elements = (selector) => {
|
||||
return client.locator(selector)
|
||||
}
|
||||
client.click = async (sel, n = 0, parent) => {
|
||||
const sl = sel + ` >> nth=${n}`
|
||||
let s = client.locator(sl)
|
||||
if (parent) {
|
||||
s = s.locator('..')
|
||||
}
|
||||
await s.waitFor({ state: 'visible', timeout: 10000 })
|
||||
await s.click()
|
||||
}
|
||||
client.elemExist = async (sel) => {
|
||||
try {
|
||||
await client.locator(sel).waitFor({ timeout: 100 })
|
||||
return true
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
client.hasElem = async (sel, target = true) => {
|
||||
const s = client.locator(sel)
|
||||
const c = await s.count()
|
||||
expect(!!c).toEqual(target)
|
||||
}
|
||||
client.countElem = async (sel) => {
|
||||
const s = client.locator(sel)
|
||||
const c = await s.count()
|
||||
return c
|
||||
}
|
||||
client.hasFocus = async (sel) => {
|
||||
const s = await client.locator(sel).nth(0)
|
||||
expect(s).toBeFocused()
|
||||
}
|
||||
client.getValue = async (sel) => {
|
||||
const s = await client.locator(sel)
|
||||
return s.inputValue()
|
||||
}
|
||||
client.getText = async (sel) => {
|
||||
const s = await client.locator(sel).nth(0)
|
||||
return s.innerText()
|
||||
}
|
||||
client.setValue = async (sel, v) => {
|
||||
const s = client.locator(sel).first()
|
||||
return s.fill(v)
|
||||
}
|
||||
client.getAttribute = async (sel, name) => {
|
||||
const element = await client.locator(sel).nth(0)
|
||||
return element.getAttribute(name)
|
||||
}
|
||||
client.rightClick = async function (sel, x, y) {
|
||||
const s = client.locator(sel).first()
|
||||
await s.waitFor({ state: 'visible', timeout: 10000 })
|
||||
await s.click({
|
||||
button: 'right',
|
||||
position: {
|
||||
x,
|
||||
y
|
||||
}
|
||||
})
|
||||
}
|
||||
client.doubleClick = async function (sel, n = 0) {
|
||||
const sl = sel + `>> nth=${n}`
|
||||
const s = client.locator(sl).first()
|
||||
await s.waitFor({ state: 'visible', timeout: 10000 })
|
||||
await s.dblclick()
|
||||
}
|
||||
client.readClipboard = async () => {
|
||||
return app.evaluate(async ({ clipboard }) => clipboard.readText())
|
||||
}
|
||||
client.writeClipboard = async (clipboardContentToWrite) => {
|
||||
await app.evaluate(async ({ clipboard }, text) => {
|
||||
await clipboard.writeText(text)
|
||||
}, clipboardContentToWrite)
|
||||
}
|
||||
client.getAttribute = async (sel, name) => {
|
||||
return client.$eval(sel, (e, name) => {
|
||||
return e.getAttribute(name)
|
||||
}, name)
|
||||
}
|
||||
client.hover = async (sel) => {
|
||||
const element = await client.locator(sel).nth(0)
|
||||
await element.hover()
|
||||
await delay(400)
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* export test username/password/host/port
|
||||
*/
|
||||
require('dotenv').config({
|
||||
override: true
|
||||
})
|
||||
const os = require('os').platform()
|
||||
const {
|
||||
env
|
||||
} = process
|
||||
|
||||
const TEST_HOST = env[`TEST_HOST_${os}`] || env.TEST_HOST
|
||||
const TEST_PASS = env[`TEST_PASS_${os}`] || env.TEST_PASS
|
||||
const TEST_USER = env[`TEST_USER_${os}`] || env.TEST_USER
|
||||
const TEST_PORT = env[`TEST_PORT_${os}`] || env.TEST_PORT || '22'
|
||||
|
||||
if (!TEST_HOST || !TEST_PASS || !TEST_USER) {
|
||||
throw new Error(`
|
||||
basic sftp test need TEST_HOST TEST_PASS TEST_USER env set,
|
||||
TEST_PORT is optional (default 22)
|
||||
you can run "cp .sample.env .env" to create env file, then edit .env, fill all required field
|
||||
`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TEST_HOST,
|
||||
TEST_PASS,
|
||||
TEST_USER,
|
||||
TEST_PORT: '' + TEST_PORT
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// expect.js
|
||||
const { expect: playwrightExpect } = require('@playwright/test')
|
||||
|
||||
function extend (value) {
|
||||
const originalExpect = playwrightExpect(value)
|
||||
|
||||
// Create a proxy to handle all method calls
|
||||
return new Proxy(originalExpect, {
|
||||
get (target, prop) {
|
||||
// Handle the five custom functions
|
||||
if (prop === 'equal' || prop === 'equals') {
|
||||
return (expected) => target.toEqual(expected)
|
||||
}
|
||||
if (prop === 'lessThan') {
|
||||
return (expected) => target.toBeLessThan(expected)
|
||||
}
|
||||
if (prop === 'greaterThan') {
|
||||
return (expected) => target.toBeGreaterThan(expected)
|
||||
}
|
||||
if (prop === 'includes') {
|
||||
return (expected) => target.toContain(expected)
|
||||
}
|
||||
|
||||
// For all other properties, return the original Playwright expect method
|
||||
return target[prop]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Export a function that looks like Chai's expect
|
||||
module.exports = {
|
||||
expect: (actual) => extend(actual)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
const { FtpSrv } = require('@electerm/ftp-srv')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
// Create FTP root directory if it doesn't exist
|
||||
const ftpRoot = path.join(__dirname, 'ftp-root')
|
||||
if (!fs.existsSync(ftpRoot)) {
|
||||
fs.mkdirSync(ftpRoot)
|
||||
}
|
||||
|
||||
const ftpServer = new FtpSrv({
|
||||
url: 'ftp://0.0.0.0:21',
|
||||
anonymous: false,
|
||||
root: ftpRoot
|
||||
})
|
||||
|
||||
// Handle user login
|
||||
ftpServer.on('login', ({ username, password }, resolve, reject) => {
|
||||
// Simple authentication
|
||||
console.log('login', username, password)
|
||||
if (username === 'test' && password === 'test123') {
|
||||
return resolve({ root: ftpRoot })
|
||||
}
|
||||
return reject(new Error('Invalid username or password'))
|
||||
})
|
||||
|
||||
// Log events
|
||||
ftpServer.on('client-error', ({ connection, context, error }) => {
|
||||
console.log('Client error', error)
|
||||
})
|
||||
|
||||
ftpServer.on('STOR', (error, fileName) => {
|
||||
if (error) {
|
||||
console.log('Error uploading file:', error)
|
||||
} else {
|
||||
console.log('File uploaded:', fileName)
|
||||
}
|
||||
})
|
||||
|
||||
ftpServer.on('RETR', (error, fileName) => {
|
||||
if (error) {
|
||||
console.log('Error downloading file:', error)
|
||||
} else {
|
||||
console.log('File downloaded:', fileName)
|
||||
}
|
||||
})
|
||||
|
||||
// Start server
|
||||
ftpServer.listen()
|
||||
.then(() => {
|
||||
console.log('FTP server is running at port 21')
|
||||
console.log('Username: test')
|
||||
console.log('Password: test123')
|
||||
console.log('FTP root directory:', ftpRoot)
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Error starting FTP server:', err)
|
||||
})
|
||||
|
||||
// Handle shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
ftpServer.close()
|
||||
.then(() => {
|
||||
console.log('FTP server closed')
|
||||
process.exit(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* some test may need only run in some os
|
||||
*/
|
||||
|
||||
const os = require('os')
|
||||
const platform = os.platform()
|
||||
|
||||
module.exports = (osName) => {
|
||||
return platform === osName
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* language fix
|
||||
*/
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
function capitalizeFirstLetter (string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
module.exports = (id) => {
|
||||
const lang = require('@electerm/electerm-locales').en_us.lang
|
||||
return capitalizeFirstLetter(_.get(lang, `${id}`) || id)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = (...arg) => {
|
||||
const timestamp = new Date().toISOString()
|
||||
console.log(`[${timestamp}] >`, ...arg)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* output uid with os name prefix
|
||||
*/
|
||||
|
||||
const { nanoid } = require('nanoid')
|
||||
const os = require('os').platform()
|
||||
|
||||
module.exports = () => {
|
||||
return os + '_' + nanoid()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* wait async
|
||||
* @param {*} time
|
||||
*/
|
||||
module.exports = time => new Promise(resolve => setTimeout(resolve, time))
|
||||
Reference in New Issue
Block a user