chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
|
||||
describe('layouts', function () {
|
||||
it('should create new tabs properly in different layouts', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
const splitMapDesc = {
|
||||
c1: 'single',
|
||||
c2: 'twoColumns',
|
||||
c3: 'threeColumns',
|
||||
r2: 'twoRows',
|
||||
r3: 'threeRows',
|
||||
c2x2: 'grid2x2',
|
||||
c1r2: 'twoRowsRight',
|
||||
r1c2: 'twoColumnsBottom'
|
||||
}
|
||||
|
||||
const splitConfig = {
|
||||
c1: {
|
||||
children: 1,
|
||||
handle: 0
|
||||
},
|
||||
c2: {
|
||||
children: 2,
|
||||
handle: 1
|
||||
},
|
||||
c3: {
|
||||
children: 3,
|
||||
handle: 2
|
||||
},
|
||||
r2: {
|
||||
children: 2,
|
||||
handle: 1
|
||||
},
|
||||
r3: {
|
||||
children: 3,
|
||||
handle: 2
|
||||
},
|
||||
c2x2: {
|
||||
children: 4,
|
||||
handle: 3
|
||||
},
|
||||
c1r2: {
|
||||
children: 3,
|
||||
handle: 2
|
||||
},
|
||||
r1c2: {
|
||||
children: 3,
|
||||
handle: 2
|
||||
}
|
||||
}
|
||||
|
||||
// Test helper function
|
||||
async function testLayout (layoutType, expectedTabCount) {
|
||||
const desc = splitMapDesc[layoutType]
|
||||
log(`Testing ${desc} layout`)
|
||||
await client.evaluate((layoutType) => {
|
||||
window.store.setLayout(layoutType)
|
||||
}, layoutType)
|
||||
await delay(1000)
|
||||
const layoutAfterCount = await client.evaluate(() => {
|
||||
return document.querySelectorAll('.layout-item').length
|
||||
})
|
||||
expect(layoutAfterCount).equal(expectedTabCount)
|
||||
}
|
||||
|
||||
// Test each layout type
|
||||
const layouts = Object.keys(splitMapDesc).reverse()
|
||||
for (const layout of layouts) {
|
||||
await testLayout(layout, splitConfig[layout].children)
|
||||
}
|
||||
let dd = await client.countElem('.ant-dropdown')
|
||||
expect(dd).equal(0)
|
||||
await client.click('.tabs .layout-dd-icon')
|
||||
await delay(500)
|
||||
dd = await client.countElem('.ant-dropdown')
|
||||
expect(dd).equal(1)
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
|
||||
describe('workspace', function () {
|
||||
it('workspace feature should work properly', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Test 1: Open layout dropdown and verify tabs exist
|
||||
log('Test 1: Opening layout dropdown')
|
||||
await client.click('.tabs .layout-dd-icon')
|
||||
await delay(500)
|
||||
const dropdown = await client.countElem('.layout-workspace-dropdown')
|
||||
expect(dropdown).equal(1)
|
||||
|
||||
// Test 2: Verify Layout tab is active by default
|
||||
log('Test 2: Verifying Layout tab is default')
|
||||
const activeTab = await client.getText('.layout-workspace-dropdown .ant-tabs-tab.ant-tabs-tab-active')
|
||||
expect(activeTab).includes('Layout')
|
||||
|
||||
// Test 3: Switch to Workspaces tab
|
||||
log('Test 3: Switching to Workspaces tab')
|
||||
await client.click('.layout-workspace-dropdown .ant-tabs-tab:has-text("Workspaces")')
|
||||
await delay(300)
|
||||
|
||||
// Test 4: Verify workspace content is shown
|
||||
log('Test 4: Verifying workspace content')
|
||||
const workspaceContent = await client.countElem('.workspace-menu-content')
|
||||
expect(workspaceContent).equal(1)
|
||||
|
||||
// Test 5: Verify save button exists
|
||||
log('Test 5: Verifying save button')
|
||||
const saveBtn = await client.countElem('.workspace-save-btn button')
|
||||
expect(saveBtn).equal(1)
|
||||
|
||||
// Test 6: Click save button to open save modal
|
||||
log('Test 6: Opening save modal')
|
||||
await client.click('.workspace-save-btn button')
|
||||
await delay(300)
|
||||
const saveModal = await client.countElem('.custom-modal-close')
|
||||
expect(saveModal).equal(1)
|
||||
|
||||
// Test 7: Verify save modal has name input
|
||||
log('Test 7: Verifying save modal input')
|
||||
const nameInput = await client.countElem('.custom-modal-wrap .ant-input')
|
||||
expect(nameInput).greaterThan(0)
|
||||
|
||||
// Test 8: Enter a workspace name and save
|
||||
log('Test 8: Saving workspace')
|
||||
const workspaceName = 'Test Workspace ' + Date.now()
|
||||
await client.setValue('.custom-modal-wrap .ant-input', workspaceName)
|
||||
await delay(200)
|
||||
await client.click('.custom-modal-wrap .ant-btn-primary:has-text("Save")')
|
||||
await delay(500)
|
||||
|
||||
// Test 9: Verify modal is closed
|
||||
log('Test 9: Verifying modal closed')
|
||||
const modalAfterSave = await client.countElem('.custom-modal-close')
|
||||
expect(modalAfterSave).equal(0)
|
||||
|
||||
// Test 11: Open dropdown again and switch to workspace tab
|
||||
log('Test 11: Reopening dropdown')
|
||||
await client.click('.tabs .layout-dd-icon')
|
||||
await delay(300)
|
||||
await client.click('.layout-workspace-dropdown .ant-tabs-tab:has-text("Workspaces")')
|
||||
await delay(300)
|
||||
|
||||
// Test 12: Verify workspace appears in the list
|
||||
log('Test 12: Verifying workspace in list')
|
||||
const workspaceItems = await client.countElem('.workspace-item')
|
||||
expect(workspaceItems).greaterThan(0)
|
||||
|
||||
// Test 13: Verify workspace name is displayed
|
||||
log('Test 13: Verifying workspace name displayed')
|
||||
const displayedName = await client.getText('.workspace-name')
|
||||
expect(displayedName).includes('Test Workspace')
|
||||
|
||||
// Test 14: Change layout then load workspace to restore
|
||||
log('Test 14: Testing workspace load')
|
||||
// First change layout to something different
|
||||
await client.click('.layout-workspace-dropdown .ant-tabs-tab:has-text("Layout")')
|
||||
await delay(300)
|
||||
await client.click('.layout-menu-item:nth-child(2)') // select c2 layout
|
||||
await delay(500)
|
||||
|
||||
// Reopen dropdown and switch back to Workspaces tab
|
||||
await client.click('.tabs .layout-dd-icon')
|
||||
await delay(300)
|
||||
await client.click('.layout-workspace-dropdown .ant-tabs-tab:has-text("Workspaces")')
|
||||
await delay(300)
|
||||
|
||||
// Now load the workspace by clicking on it
|
||||
await client.click('.workspace-item')
|
||||
await delay(500)
|
||||
|
||||
// Test 15: Delete workspace
|
||||
log('Test 15: Testing workspace delete')
|
||||
// Reopen dropdown
|
||||
await client.click('.tabs .layout-dd-icon')
|
||||
await delay(300)
|
||||
await client.click('.layout-workspace-dropdown .ant-tabs-tab:has-text("Workspaces")')
|
||||
await delay(300)
|
||||
|
||||
// Click delete icon
|
||||
const deleteIcon = await client.countElem('.workspace-delete-icon')
|
||||
if (deleteIcon > 0) {
|
||||
await client.click('.workspace-delete-icon')
|
||||
await delay(300)
|
||||
|
||||
// Confirm delete
|
||||
await client.click('.ant-popconfirm .ant-btn-primary')
|
||||
await delay(500)
|
||||
|
||||
// Verify workspace deleted
|
||||
const remainingWorkspaces = await client.countElem('.workspace-item')
|
||||
expect(remainingWorkspaces).equal(0)
|
||||
}
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it, expect } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
const delay = require('./common/wait')
|
||||
const { nanoid } = require('nanoid')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('Web session', function () {
|
||||
it('should create a web bookmark and verify history and webview', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Get initial history count
|
||||
const initialHistoryCount = await client.evaluate(() => {
|
||||
return window.store.history.length
|
||||
})
|
||||
|
||||
// Open bookmark form
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(500)
|
||||
|
||||
// Select web bookmark type
|
||||
await client.click('.setting-wrap .ant-radio-button-wrapper:has-text("Web")')
|
||||
await delay(500)
|
||||
|
||||
// Generate test data
|
||||
const bookmarkTitle = 'Web-' + nanoid()
|
||||
const testUrl = 'https://github.com'
|
||||
const type = 'web'
|
||||
|
||||
// Fill in bookmark details
|
||||
await client.setValue('.setting-wrap input[id="web-form_title"]', bookmarkTitle)
|
||||
await client.setValue('.setting-wrap input[id="web-form_url"]', testUrl)
|
||||
|
||||
// Save and connect
|
||||
await client.click('.setting-wrap .ant-btn-primary')
|
||||
await delay(2000)
|
||||
|
||||
// Verify history has been updated
|
||||
const historyItem = await client.evaluate(() => {
|
||||
const history = window.store.history
|
||||
return history[0]
|
||||
})
|
||||
|
||||
expect(historyItem.tab.title).toEqual(bookmarkTitle)
|
||||
expect(historyItem.tab.type).toEqual(type)
|
||||
expect(historyItem.tab.url).toEqual(testUrl)
|
||||
|
||||
// Verify history count has increased
|
||||
const newHistoryCount = await client.evaluate(() => {
|
||||
return window.store.history.length
|
||||
})
|
||||
expect(newHistoryCount).toEqual(initialHistoryCount + 1)
|
||||
|
||||
// Verify webview loaded the correct URL
|
||||
await delay(2000)
|
||||
const webviewUrl = await client.evaluate(() => {
|
||||
const webview = document.querySelector('webview')
|
||||
return webview ? webview.getAttribute('src') : ''
|
||||
})
|
||||
expect(webviewUrl).toEqual(testUrl + '/')
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it, expect } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
const delay = require('./common/wait')
|
||||
const { nanoid } = require('nanoid')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { basicTerminalTest } = require('./common/basic-terminal-test')
|
||||
|
||||
describe('Telnet bookmark', function () {
|
||||
it('should create a telnet bookmark and verify connection', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Get initial history count
|
||||
const initialHistoryCount = await client.evaluate(() => {
|
||||
return window.store.history.length
|
||||
})
|
||||
|
||||
// Open bookmark form
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(500)
|
||||
|
||||
// Select telnet bookmark type
|
||||
await client.click('.setting-wrap .ant-radio-button-wrapper:has-text("Telnet")')
|
||||
await delay(500)
|
||||
|
||||
// Generate a unique title for the bookmark
|
||||
const bookmarkTitle = `Telnet-${nanoid()}`
|
||||
const testHost = 'telehack.com'
|
||||
const testPort = '23' // standard telnet port
|
||||
const type = 'telnet'
|
||||
|
||||
// Fill in bookmark details using SSH form IDs
|
||||
await client.setValue('#telnet-form_host', testHost)
|
||||
await client.setValue('#telnet-form_title', bookmarkTitle)
|
||||
await client.setValue('#telnet-form_port', testPort)
|
||||
|
||||
// Save and connect
|
||||
await client.click('.setting-wrap .ant-btn-primary')
|
||||
await delay(5000) // Need longer delay for telnet connection
|
||||
|
||||
// Verify that the history has been updated
|
||||
const historyItem = await client.evaluate(() => {
|
||||
const history = window.store.history
|
||||
return history[0]
|
||||
})
|
||||
expect(historyItem.tab.title).toEqual(bookmarkTitle)
|
||||
expect(historyItem.tab.host).toEqual(testHost)
|
||||
expect(historyItem.tab.port).toEqual(Number(testPort))
|
||||
expect(historyItem.tab.type).toEqual(type)
|
||||
|
||||
// Verify history count has increased
|
||||
const newHistoryCount = await client.evaluate(() => {
|
||||
return window.store.history.length
|
||||
})
|
||||
expect(newHistoryCount).toEqual(initialHistoryCount + 1)
|
||||
|
||||
// Verify terminal has connected and shows some output
|
||||
await delay(2000)
|
||||
await basicTerminalTest(client, 'help')
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it } = require('@playwright/test')
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
it('should toggle split view and control tabs correctly', async () => {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
|
||||
await delay(3500)
|
||||
|
||||
const terminalSection = client.locator('.term-wrap')
|
||||
const sftpSection = client.locator('.sftp-section')
|
||||
const splitViewToggle = client.locator('.session-current .split-view-toggle')
|
||||
const sftpFollowSshIcon = client.locator('.sftp-follow-ssh-icon')
|
||||
|
||||
// Check initial state
|
||||
await expect(terminalSection).toBeVisible()
|
||||
await expect(sftpSection).toBeHidden()
|
||||
await expect(splitViewToggle).toBeVisible()
|
||||
await expect(sftpFollowSshIcon).toBeVisible()
|
||||
|
||||
// Click split view toggle
|
||||
await splitViewToggle.click()
|
||||
await delay(1000)
|
||||
|
||||
// After toggle: both terminal and sftp should be visible
|
||||
await expect(terminalSection).toBeVisible()
|
||||
await expect(sftpSection).toBeVisible()
|
||||
await expect(splitViewToggle).toBeVisible()
|
||||
await expect(sftpFollowSshIcon).toBeVisible()
|
||||
|
||||
// Click split view toggle again
|
||||
await splitViewToggle.click()
|
||||
await delay(1000)
|
||||
|
||||
// After second toggle: should return to initial state
|
||||
await expect(terminalSection).toBeVisible()
|
||||
await expect(sftpSection).toBeHidden()
|
||||
await expect(splitViewToggle).toBeVisible()
|
||||
await expect(sftpFollowSshIcon).toBeVisible()
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it } = require('@playwright/test')
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
it('should respect default split view setting when creating new tabs', async () => {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Check initial state (split view off by default)
|
||||
const terminalSection = client.locator('.session-current .term-wrap')
|
||||
const sftpSection = client.locator('.session-current .sftp-section')
|
||||
|
||||
await expect(terminalSection).toBeVisible()
|
||||
await expect(sftpSection).toBeHidden()
|
||||
|
||||
// Change default split view setting to true
|
||||
await client.evaluate(() => {
|
||||
window.store._config.sshSftpSplitView = true
|
||||
})
|
||||
await delay(500)
|
||||
|
||||
// Create new tab
|
||||
await client.click('.tabs-add-btn')
|
||||
await delay(500)
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(1000)
|
||||
|
||||
// New tab should open with split view enabled
|
||||
await expect(terminalSection).toBeVisible()
|
||||
await expect(sftpSection).toBeVisible()
|
||||
|
||||
// Change setting back to false
|
||||
await client.evaluate(() => {
|
||||
window.store._config.sshSftpSplitView = false
|
||||
})
|
||||
await delay(500)
|
||||
|
||||
// Create another new tab
|
||||
await client.click('.tabs-add-btn')
|
||||
await delay(500)
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(1000)
|
||||
|
||||
// New tab should open with split view disabled
|
||||
await expect(terminalSection).toBeVisible()
|
||||
await expect(sftpSection).toBeHidden()
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
@@ -0,0 +1,128 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it, expect } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { spawn } = require('child_process')
|
||||
const path = require('path')
|
||||
|
||||
describe('AI Config and Suggestions', function () {
|
||||
let aiServer
|
||||
let electronApp
|
||||
let client
|
||||
|
||||
// Start AI API server before all tests
|
||||
it.beforeAll(async () => {
|
||||
const serverPath = path.join(__dirname, 'common', 'ai-api.js')
|
||||
aiServer = spawn('node', [serverPath])
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait for server to start
|
||||
})
|
||||
|
||||
// Stop AI API server after all tests
|
||||
it.afterAll(() => {
|
||||
if (aiServer) {
|
||||
aiServer.kill()
|
||||
}
|
||||
})
|
||||
|
||||
it.beforeEach(async () => {
|
||||
electronApp = await electron.launch(appOptions)
|
||||
client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
})
|
||||
|
||||
it.afterEach(async () => {
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
showCmdSuggestions: false
|
||||
})
|
||||
})
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
it('should open AI setting page and fill configuration', async function () {
|
||||
// Click AI button to open settings
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
showCmdSuggestions: true
|
||||
})
|
||||
})
|
||||
await client.click('.terminal-footer-ai .ai-icon')
|
||||
await delay(1000)
|
||||
|
||||
// Verify AI setting page is open
|
||||
await expect(client.locator('.ai-config-modal .ai-config-form')).toBeVisible()
|
||||
|
||||
// Fill in the AI configuration form
|
||||
await client.fill('#baseURLAI', 'http://localhost:43434')
|
||||
await client.fill('#apiPathAI', '/chat/completions')
|
||||
await client.fill('#modelAI', 'gpt-3.5-turbo')
|
||||
await client.fill('#apiKeyAI', 'test-api-key')
|
||||
await client.fill('#roleAI', 'You are a helpful assistant')
|
||||
await client.fill('#languageAI', 'English')
|
||||
|
||||
// Save the configuration
|
||||
await client.click('.ai-config-form button[type="submit"]')
|
||||
await delay(1000)
|
||||
|
||||
// Verify the setting panel is closed
|
||||
await expect(client.locator('.setting-wrap')).not.toBeVisible()
|
||||
})
|
||||
|
||||
it('should verify AI functionality after configuration', async function () {
|
||||
// Click AI button after configuration
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
showCmdSuggestions: true
|
||||
})
|
||||
})
|
||||
await client.click('.terminal-footer-ai .ai-icon')
|
||||
await delay(1000)
|
||||
|
||||
// Verify that the AI setting page does not open this time
|
||||
await expect(client.locator('.ai-config-modal .ai-config-form')).not.toBeVisible()
|
||||
|
||||
// Verify that AI functionality is triggered instead
|
||||
await expect(client.locator('.ai-chat-container')).toBeVisible()
|
||||
})
|
||||
|
||||
it('should test AI suggestions functionality', async function () {
|
||||
// Open a terminal or ensure we're in a context where we can input commands
|
||||
// You might need to add steps here to open a terminal tab if it's not open by default
|
||||
// Click AI button after configuration
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
showCmdSuggestions: true
|
||||
})
|
||||
})
|
||||
// Input a command
|
||||
const testCommand = 'test'
|
||||
await client.keyboard.type(testCommand)
|
||||
await delay(100)
|
||||
await client.keyboard.press('ArrowRight')
|
||||
await delay(100)
|
||||
await client.keyboard.press('ArrowRight')
|
||||
await delay(500)
|
||||
|
||||
// Get the initial count of suggestions
|
||||
const initialSuggestionsCount = await client.locator('.suggestion-item').count()
|
||||
|
||||
// Click the "Get AI suggestions" button
|
||||
await client.click('.terminal-suggestions-sticky div:has-text("Get AI suggestions")')
|
||||
await delay(2000) // Wait for suggestions to load
|
||||
|
||||
// Get the new count of suggestions
|
||||
const newSuggestionsCount = await client.locator('.suggestion-item').count()
|
||||
|
||||
// Verify that the number of suggestions has increased
|
||||
expect(newSuggestionsCount).toBeGreaterThan(initialSuggestionsCount)
|
||||
|
||||
// Verify that the new suggestions start with the input command
|
||||
const suggestions = await client.locator('.suggestion-item').allTextContents()
|
||||
for (const suggestion of suggestions) {
|
||||
expect(suggestion.startsWith(testCommand)).toBeTruthy()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* basic ssh test
|
||||
* need TEST_HOST TEST_PASS TEST_USER env set
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const { basicTerminalTest } = require('./common/basic-terminal-test')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { setupSshConnection } = require('./common/common')
|
||||
|
||||
describe('ssh', function () {
|
||||
it('should open window and basic ssh ls command works', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
const cmd = 'ls'
|
||||
await setupSshConnection(client)
|
||||
await delay(5500)
|
||||
let tabsCount = await client.elements('.tabs .tabs-wrapper .tab')
|
||||
tabsCount = await tabsCount.count()
|
||||
expect(tabsCount).equal(2)
|
||||
await delay(4010)
|
||||
await basicTerminalTest(client, cmd)
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it, expect } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('Terminal Suggestions Dropdown', function () {
|
||||
let electronApp
|
||||
let client
|
||||
it.beforeEach(async () => {
|
||||
electronApp = await electron.launch(appOptions)
|
||||
client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
showCmdSuggestions: true
|
||||
})
|
||||
})
|
||||
})
|
||||
it.afterAll(async () => {
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
showCmdSuggestions: false
|
||||
})
|
||||
})
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
it('should show suggestions based on command history and handle deletion', async function () {
|
||||
await delay(1500)
|
||||
// Type the partial command and check initial suggestions count
|
||||
const partialCommand = 'test-unique'
|
||||
await client.keyboard.type(partialCommand)
|
||||
await delay(100)
|
||||
await client.keyboard.press('ArrowRight')
|
||||
await delay(100)
|
||||
await client.keyboard.press('ArrowRight')
|
||||
|
||||
// Verify suggestions panel is visible
|
||||
const suggestionElement = await client.locator('.terminal-suggestions-wrap').first()
|
||||
await expect(suggestionElement).toBeVisible()
|
||||
|
||||
// Count initial suggestions for our partial command
|
||||
const initialSuggestions = await client.locator('.suggestion-item').count()
|
||||
|
||||
// Continue typing to make it a unique command
|
||||
await client.keyboard.type('-command-' + Date.now())
|
||||
|
||||
// Verify AI suggestions button
|
||||
const aiSuggestionsButton = await client.locator('.terminal-suggestions-sticky div').first()
|
||||
await expect(aiSuggestionsButton).toBeVisible()
|
||||
await expect(aiSuggestionsButton).toHaveText('Get AI suggestions')
|
||||
|
||||
// Press Enter to execute command
|
||||
await client.keyboard.press('Enter')
|
||||
await expect(suggestionElement).toBeHidden()
|
||||
|
||||
await delay(1000)
|
||||
|
||||
// Type the same partial command again
|
||||
await client.keyboard.type(partialCommand)
|
||||
await delay(100)
|
||||
await client.keyboard.press('ArrowRight')
|
||||
await delay(100)
|
||||
await client.keyboard.press('ArrowRight')
|
||||
|
||||
// Verify suggestions are visible again
|
||||
// The suggestions list should filter commands that start with the partial input
|
||||
await expect(suggestionElement).toBeVisible()
|
||||
|
||||
// Verify suggestion count increased for the same partial command
|
||||
const newSuggestionsCount = await client.locator('.suggestion-item').count()
|
||||
expect(newSuggestionsCount).toBeGreaterThan(initialSuggestions)
|
||||
|
||||
// Press Enter to close suggestions
|
||||
await client.keyboard.press('Enter')
|
||||
await expect(suggestionElement).toBeHidden()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,82 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it, expect } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { spawn } = require('child_process')
|
||||
const path = require('path')
|
||||
|
||||
describe('AI Config and Suggestions', function () {
|
||||
let aiServer
|
||||
let electronApp
|
||||
let client
|
||||
|
||||
// Start AI API server before all tests
|
||||
it.beforeAll(async () => {
|
||||
const serverPath = path.join(__dirname, 'common', 'ai-api.js')
|
||||
aiServer = spawn('node', [serverPath])
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait for server to start
|
||||
})
|
||||
|
||||
// Stop AI API server after all tests
|
||||
it.afterAll(() => {
|
||||
if (aiServer) {
|
||||
aiServer.kill()
|
||||
}
|
||||
})
|
||||
|
||||
it.beforeEach(async () => {
|
||||
electronApp = await electron.launch(appOptions)
|
||||
client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
})
|
||||
|
||||
it.afterEach(async () => {
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
it('should verify AI functionality after configuration', async function () {
|
||||
// Open AI chat
|
||||
await client.click('.terminal-footer-ai .ai-icon')
|
||||
await delay(1000)
|
||||
|
||||
// Verify AI chat is open
|
||||
await expect(client.locator('.ai-chat-container')).toBeVisible()
|
||||
|
||||
// Get initial chat history count
|
||||
const initialHistoryCount = await client.locator('.chat-history-item').count()
|
||||
|
||||
// Enter a test prompt
|
||||
const testPrompt = 'Hello, AI assistant! Please provide a long response.'
|
||||
await client.fill('.ai-chat-textarea', testPrompt)
|
||||
|
||||
// Submit the prompt
|
||||
await client.click('.ai-chat-terminals .anticon-send')
|
||||
|
||||
// Wait for response (adjust delay if needed)
|
||||
await delay(5000)
|
||||
|
||||
// Get new chat history count
|
||||
const newHistoryCount = await client.locator('.chat-history-item').count()
|
||||
|
||||
// Verify chat history increased by 1
|
||||
expect(newHistoryCount).toBe(initialHistoryCount + 1)
|
||||
|
||||
// Verify the last chat history item contains the test prompt
|
||||
const lastChatItem = await client.locator('.chat-history-item').last()
|
||||
const promptContent = await lastChatItem.locator('.ant-alert-title').textContent()
|
||||
expect(promptContent).toContain(testPrompt)
|
||||
|
||||
// Test clear history functionality
|
||||
await client.click('.ai-chat-terminals .clear-ai-icon')
|
||||
await delay(500)
|
||||
await client.click('.ant-popover .ant-btn-primary')
|
||||
await delay(1000)
|
||||
|
||||
// Verify that the chat history is now empty
|
||||
const finalHistoryCount = await client.locator('.chat-history-item').count()
|
||||
expect(finalHistoryCount).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it, expect } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { spawn } = require('child_process')
|
||||
const path = require('path')
|
||||
|
||||
describe('Terminal Explain with AI', function () {
|
||||
let aiServer
|
||||
let electronApp
|
||||
let client
|
||||
|
||||
// Start AI API server before all tests
|
||||
it.beforeAll(async () => {
|
||||
const serverPath = path.join(__dirname, 'common', 'ai-api.js')
|
||||
aiServer = spawn('node', [serverPath])
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)) // Wait for server to start
|
||||
})
|
||||
|
||||
// Stop AI API server after all tests
|
||||
it.afterAll(() => {
|
||||
if (aiServer) {
|
||||
aiServer.kill()
|
||||
}
|
||||
})
|
||||
|
||||
it.beforeEach(async () => {
|
||||
electronApp = await electron.launch(appOptions)
|
||||
client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
})
|
||||
|
||||
it.afterEach(async () => {
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
showCmdSuggestions: false
|
||||
})
|
||||
})
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
it('should explain selected text with AI', async function () {
|
||||
// Type some text in the terminal
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
showCmdSuggestions: true
|
||||
})
|
||||
})
|
||||
const testCommand = 'ls -la'
|
||||
await client.type('.xterm-helper-textarea', testCommand)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(1000)
|
||||
|
||||
// Use terminal's built-in select all command (usually Cmd+A or Ctrl+A)
|
||||
await client.keyboard.press(process.platform === 'darwin' ? 'Meta+A' : 'Control+A')
|
||||
await delay(500)
|
||||
|
||||
// Right-click to open context menu
|
||||
await client.rightClick('.term-wrap', 10, 10)
|
||||
await delay(500)
|
||||
|
||||
// Click "Explain with AI" in the context menu
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Explain with AI")')
|
||||
|
||||
// Verify that the AI panel is opened
|
||||
await expect(client.locator('.ai-chat-container')).toBeVisible()
|
||||
|
||||
// Verify that the selected text is sent to AI for explanation
|
||||
// await delay(500)
|
||||
// const aiChatTextarea = client.locator('.ai-chat-textarea')
|
||||
// await expect(aiChatTextarea).toHaveValue('explain terminal output')
|
||||
|
||||
// Verify that the AI response is received
|
||||
await delay(3000) // Wait for AI response
|
||||
const aiResponse = client.locator('.chat-history-item:last-child')
|
||||
await expect(aiResponse).toBeVisible()
|
||||
|
||||
// Check if the response contains some expected content
|
||||
await expect(aiResponse).toContainText('Response to your query', { timeout: 10000 })
|
||||
|
||||
// Verify that the response is not empty
|
||||
const responseText = await aiResponse.textContent()
|
||||
expect(responseText.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* info panel test
|
||||
* need TEST_HOST TEST_PASS TEST_USER env set
|
||||
*/
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const { TEST_HOST } = require('./common/env')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { setupSshConnection } = require('./common/common')
|
||||
|
||||
describe('info panel', function () {
|
||||
it('info panel should work in both local and ssh terminals', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
|
||||
// Get initial activeTabId
|
||||
const initialTabId = await client.evaluate(() => {
|
||||
return window.store.activeTabId
|
||||
})
|
||||
|
||||
// Test local terminal info panel
|
||||
log('click info icon for local terminal')
|
||||
await client.click('.terminal-footer-info .terminal-info-icon')
|
||||
await delay(1000)
|
||||
|
||||
await client.hasElem('.right-side-panel')
|
||||
|
||||
let panelContent = await client.getText('.right-side-panel-content')
|
||||
expect(panelContent).includes(initialTabId)
|
||||
|
||||
// Create SSH connection
|
||||
log('create ssh connection')
|
||||
await setupSshConnection(client)
|
||||
await delay(8500)
|
||||
|
||||
// Get SSH tab ID and verify info panel content
|
||||
const sshTabId = await client.evaluate(() => {
|
||||
return window.store.activeTabId
|
||||
})
|
||||
expect(sshTabId !== initialTabId).equal(true)
|
||||
|
||||
panelContent = await client.getText('.right-side-panel-content')
|
||||
expect(panelContent).includes(sshTabId)
|
||||
|
||||
// Switch back to local tab
|
||||
log('switch back to local tab')
|
||||
await client.click('.tabs .tab', 0)
|
||||
await delay(1000)
|
||||
|
||||
panelContent = await client.getText('.right-side-panel-content')
|
||||
expect(panelContent).includes(initialTabId)
|
||||
expect(panelContent.includes(TEST_HOST)).equal(false)
|
||||
|
||||
// Close info panel
|
||||
log('close info panel')
|
||||
await client.click('.right-side-panel-close')
|
||||
await delay(500)
|
||||
|
||||
const panelCount = await client.countElem('.right-side-panel')
|
||||
expect(panelCount).equal(0)
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const { expect } = require('./common/expect')
|
||||
const appOptions = require('./common/app-options')
|
||||
const {
|
||||
TEST_HOST,
|
||||
TEST_PASS,
|
||||
TEST_USER,
|
||||
TEST_PORT
|
||||
} = require('./common/env')
|
||||
const e = require('./common/lang')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('bookmarks', function () {
|
||||
it('all buttons open proper bookmark tab', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(5500)
|
||||
|
||||
log('button:edit')
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(2500)
|
||||
const sel = '.setting-wrap .ant-tabs-nav-list .ant-tabs-tab-active'
|
||||
await client.hasElem(sel)
|
||||
const text = await client.getText(sel)
|
||||
expect(text).equal(e('bookmarks'))
|
||||
|
||||
log('auto focus works')
|
||||
await client.hasFocus('.setting-wrap #ssh-form_host')
|
||||
|
||||
log('default username = ""')
|
||||
const v = await client.getValue('.setting-wrap #ssh-form_username')
|
||||
expect(v).equal('')
|
||||
|
||||
log('default port = 22')
|
||||
const v1 = await client.getValue('.setting-wrap #ssh-form_port')
|
||||
expect(v1).equal('22')
|
||||
|
||||
log('save it')
|
||||
const bookmarkCountPrev = await client.evaluate(() => {
|
||||
return window.store.bookmarks.length
|
||||
})
|
||||
await client.setValue('.setting-wrap #ssh-form_host', TEST_HOST)
|
||||
await client.setValue('.setting-wrap #ssh-form_username', TEST_USER)
|
||||
await client.setValue('.setting-wrap #ssh-form_password', TEST_PASS)
|
||||
await client.setValue('.setting-wrap #ssh-form_port', TEST_PORT)
|
||||
// const list0 = await client.elements('.setting-wrap .tree-item')
|
||||
await client.click('.setting-wrap .ant-btn-primary')
|
||||
await delay(1000)
|
||||
const bookmarkCount = await client.evaluate(() => {
|
||||
return window.store.bookmarks.length
|
||||
})
|
||||
// const list = await client.elements('.setting-wrap .tree-item')
|
||||
// await delay(100)
|
||||
expect(bookmarkCount).equal(bookmarkCountPrev + 1)
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const os = require('os')
|
||||
const delay = require('./common/wait')
|
||||
const { basicTerminalTest } = require('./common/basic-terminal-test')
|
||||
const platform = os.platform()
|
||||
const isWin = platform.startsWith('win')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
// if (!process.env.LOCAL_TEST && isOs('darwin')) {
|
||||
// return
|
||||
// }
|
||||
|
||||
describe('terminal', function () {
|
||||
it('should open window and local terminal ls/dir command works', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
const cmd = isWin
|
||||
? 'dir'
|
||||
: 'ls'
|
||||
await delay(13500)
|
||||
await basicTerminalTest(client, cmd)
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const log = require('./common/log')
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const nanoid = require('./common/uid')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const {
|
||||
closeApp,
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteItem,
|
||||
enterFolder,
|
||||
navigateToParentFolder,
|
||||
selectAllContextMenu,
|
||||
setupSftpConnection,
|
||||
verifyCurrentPath,
|
||||
verifyFileExists,
|
||||
verifyFileNotExists
|
||||
} = require('./common/common')
|
||||
|
||||
describe('local file manager', function () {
|
||||
it('should open window and basic sftp works', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// make a local folder
|
||||
await setupSftpConnection(client)
|
||||
log('009 -> add folder')
|
||||
const fname = '00000test-electerm' + nanoid()
|
||||
await createFolder(client, 'local', fname)
|
||||
expect(await verifyFileExists(client, 'local', fname)).equal(true)
|
||||
|
||||
// enter folder
|
||||
await enterFolder(client, 'local', fname)
|
||||
expect(await verifyCurrentPath(client, 'local', fname)).equal(true)
|
||||
|
||||
// new file
|
||||
log('009 -> add file')
|
||||
const fname00 = '00000test-electerm' + nanoid()
|
||||
await createFile(client, 'local', fname00)
|
||||
expect(await verifyFileExists(client, 'local', fname00)).equal(true)
|
||||
|
||||
// select all and del Control
|
||||
log('009 -> select all')
|
||||
await selectAllContextMenu(client, 'local')
|
||||
await client.keyboard.press('Delete')
|
||||
await delay(420)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(4000)
|
||||
expect(await verifyFileNotExists(client, 'local', fname00)).equal(true)
|
||||
|
||||
// goto parent
|
||||
log('009 -> goto parent')
|
||||
await navigateToParentFolder(client, 'local')
|
||||
expect(await verifyFileExists(client, 'local', fname)).equal(true)
|
||||
|
||||
// del folder
|
||||
log('009 -> del folder')
|
||||
await deleteItem(client, 'local', fname)
|
||||
expect(await verifyFileNotExists(client, 'local', fname)).equal(true)
|
||||
await closeApp(electronApp, __filename)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* quick commands test
|
||||
* need TEST_HOST TEST_PASS TEST_USER env set
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('quick commands', function () {
|
||||
it('quick commands form', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
log('open setting')
|
||||
await delay(2000)
|
||||
await client.click('.btns .anticon-setting')
|
||||
await delay(2500)
|
||||
log('click quick commands')
|
||||
await client.click('.setting-tabs [role="tab"]', 3)
|
||||
// await client.click('.setting-tabs [role="tab"]', 4)
|
||||
await client.setValue(
|
||||
'.setting-tabs-quick-commands input#name',
|
||||
'ls'
|
||||
)
|
||||
await client.setValue(
|
||||
'.setting-tabs-quick-commands textarea.ant-input',
|
||||
'ls'
|
||||
)
|
||||
const qmlist1 = await client.countElem('.setting-tabs-quick-commands .item-list-unit')
|
||||
await delay(150)
|
||||
await client.click('.setting-tabs-quick-commands .ant-btn-primary')
|
||||
await delay(2550)
|
||||
const qmlist2 = await client.countElem('.setting-tabs-quick-commands .item-list-unit')
|
||||
expect(qmlist2).equal(qmlist1 + 1)
|
||||
await client.evaluate(() => {
|
||||
window.store.updateTab(window.store.activeTabId, {
|
||||
quickCommands: [
|
||||
{
|
||||
name: 'xx',
|
||||
command: 'ls'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
await delay(1150)
|
||||
const c1 = await client.evaluate(() => {
|
||||
return window.store.quickCommands.length
|
||||
})
|
||||
const c2 = await client.evaluate(() => {
|
||||
return window.store.currentQuickCommands.length
|
||||
})
|
||||
expect(c2).equal(c1 + 1)
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* quick commands execution test
|
||||
*/
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it,
|
||||
expect
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { getTerminalContent } = require('./common/basic-terminal-test')
|
||||
|
||||
describe('quick commands execution', function () {
|
||||
it('should execute quick command when clicked in quick command box', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
|
||||
await delay(3500)
|
||||
|
||||
const initialContent = await getTerminalContent(client)
|
||||
await client.evaluate(() => {
|
||||
window.store.addQuickCommand({
|
||||
name: 'ls',
|
||||
commands: [
|
||||
{
|
||||
command: 'ls',
|
||||
id: Date.now() + '',
|
||||
delay: 100
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
// Open quick command box by hovering the trigger
|
||||
log('open quick command box')
|
||||
await client.hover('.quick-command-trigger-wrap .ant-btn')
|
||||
await delay(1000)
|
||||
|
||||
// Verify quick command box is visible
|
||||
// const quickCommandBox = await client.element('.quick-command-box')
|
||||
// await expect(quickCommandBox).toBeVisible()
|
||||
|
||||
// Click the quick command created in previous test
|
||||
log('execute quick command')
|
||||
await client.click('.qm-item')
|
||||
await delay(1000)
|
||||
|
||||
// Get new terminal content
|
||||
const newContent = await getTerminalContent(client)
|
||||
|
||||
// Verify command was executed
|
||||
await expect(newContent.length).toBeGreaterThan(initialContent.length)
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const log = require('./common/log')
|
||||
const { expect } = require('./common/expect')
|
||||
|
||||
describe('auto upgrade check', function () {
|
||||
it('auto upgrade check should work', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
let v = ''
|
||||
while (v !== '0.0.0') {
|
||||
v = await client.evaluate(() => {
|
||||
if (window.et) {
|
||||
console.log('no retry set version')
|
||||
window.et.version = '0.0.0'
|
||||
return '0.0.0'
|
||||
}
|
||||
console.log('retry set version')
|
||||
return ''
|
||||
})
|
||||
await delay(2)
|
||||
}
|
||||
console.log('v', v)
|
||||
const len1 = 10000
|
||||
const sel = '.animate.upgrade-panel'
|
||||
for (let i = 0; i < len1; i++) {
|
||||
await delay(500)
|
||||
if (await client.elemExist(sel)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
log('should show upgrade info')
|
||||
log('start download upgrade')
|
||||
await client.click('.upgrade-panel .ant-btn-primary')
|
||||
const fr = {}
|
||||
const len = 200
|
||||
for (let i = 0; i < len; i++) {
|
||||
await delay(500)
|
||||
const txt = await client.getText('.upgrade-panel .ant-btn-primary')
|
||||
console.log('txt', txt)
|
||||
if (txt.includes('Upgrading... 0% Cancel')) {
|
||||
fr.zero = 1
|
||||
} else if (
|
||||
txt.includes('% Cancel')
|
||||
) {
|
||||
fr.progress = 1
|
||||
}
|
||||
if (fr.zero && fr.progress) {
|
||||
break
|
||||
}
|
||||
}
|
||||
expect(fr.progress).equal(1)
|
||||
expect(fr.zero).equal(1)
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('Upgrade check', function () {
|
||||
it('Upgrade check should work', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
|
||||
log('button:about')
|
||||
await client.click('.btns .open-about-icon')
|
||||
await delay(2500)
|
||||
const sel = '.custom-modal-wrap .ant-tabs-nav-list .ant-tabs-tab-active'
|
||||
await client.hasElem(sel)
|
||||
|
||||
await client.click('.about-wrap .ant-btn-primary')
|
||||
await delay(500)
|
||||
await client.hasElem('.update-msg')
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFolder,
|
||||
deleteItem,
|
||||
selectAllContextMenu
|
||||
} = require('./common/common')
|
||||
|
||||
describe('File List Context Menu Select All Operation', function () {
|
||||
it('should select all items using context menu and verify single click behavior for both local and remote file lists', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Set up SSH connection first for remote testing
|
||||
await setupSftpConnection(client)
|
||||
|
||||
// Test for both local and remote
|
||||
await testSelectAll(client, 'local')
|
||||
await testSelectAll(client, 'remote')
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
})
|
||||
|
||||
async function testSelectAll (client, type) {
|
||||
// Create two test folders
|
||||
const folderName1 = `test-folder-1-${Date.now()}`
|
||||
const folderName2 = `test-folder-2-${Date.now()}`
|
||||
|
||||
await createFolder(client, type, folderName1)
|
||||
await createFolder(client, type, folderName2)
|
||||
|
||||
// Select all items using context menu
|
||||
await selectAllContextMenu(client, type)
|
||||
|
||||
// Check if all real file items have the 'selected' class
|
||||
let fileItems = await client.locator(`.session-current .file-list.${type} .real-file-item`)
|
||||
let count = await fileItems.count()
|
||||
expect(count).toBeGreaterThanOrEqual(2)
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hasSelectedClass = await fileItems.nth(i).evaluate(el => el.classList.contains('selected'))
|
||||
expect(hasSelectedClass).toBe(true)
|
||||
}
|
||||
|
||||
// Click on a single file item to deselect all except the clicked one
|
||||
await client.click(`.session-current .file-list.${type} .real-file-item`)
|
||||
await delay(500)
|
||||
|
||||
// Check that only the clicked item has the 'selected' class
|
||||
fileItems = await client.locator(`.session-current .file-list.${type} .real-file-item`)
|
||||
count = await fileItems.count()
|
||||
let selectedCount = 0
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hasSelectedClass = await fileItems.nth(i).evaluate(el => el.classList.contains('selected'))
|
||||
if (hasSelectedClass) {
|
||||
selectedCount++
|
||||
}
|
||||
}
|
||||
expect(selectedCount).toBe(1)
|
||||
|
||||
// Deselect all for the next test by clicking on empty space
|
||||
await client.click(`.session-current .file-list.${type}`)
|
||||
await delay(500)
|
||||
|
||||
// Clean up - delete the test folders
|
||||
await deleteItem(client, type, folderName1)
|
||||
await deleteItem(client, type, folderName2)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const { getTerminalContent } = require('./common/basic-terminal-test')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFolder,
|
||||
deleteItem,
|
||||
accessFolderFromTerminal,
|
||||
renameItem,
|
||||
enterFolder,
|
||||
navigateToParentFolder,
|
||||
verifyFileExists,
|
||||
verifyCurrentPath,
|
||||
clickSftpTab,
|
||||
countFileListItems
|
||||
} = require('./common/common')
|
||||
|
||||
describe('file-item-context-menu', function () {
|
||||
it('should test gotoFolderInTerminal function', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Click sftp tab first
|
||||
await clickSftpTab(client)
|
||||
|
||||
// Create a new folder
|
||||
const folderName = 'test-folder-' + Date.now()
|
||||
await createFolder(client, 'local', folderName)
|
||||
|
||||
// Access folder from terminal
|
||||
await accessFolderFromTerminal(client, 'local', folderName)
|
||||
|
||||
// Verify terminal has cd command and folder name
|
||||
const terminalContent = await getTerminalContent(client)
|
||||
expect(terminalContent.includes('cd ')).toBe(true)
|
||||
expect(terminalContent.includes(folderName)).toBe(true)
|
||||
|
||||
// Clean up - delete the test folder
|
||||
await clickSftpTab(client)
|
||||
await deleteItem(client, 'local', folderName)
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
it('should test gotoFolderInTerminal function in SSH session', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Set up SSH connection
|
||||
await setupSftpConnection(client)
|
||||
|
||||
// Create a new folder in local first
|
||||
const localFolderName = 'test-local-folder-' + Date.now()
|
||||
await createFolder(client, 'local', localFolderName)
|
||||
|
||||
// Create a new folder in remote
|
||||
const remoteFolderName = 'test-ssh-folder-' + Date.now()
|
||||
await createFolder(client, 'remote', remoteFolderName)
|
||||
|
||||
// Verify local folder does not have "Access this folder from terminal" option
|
||||
await client.rightClick(`.file-list.local .sftp-item[title="${localFolderName}"]`, 10, 10)
|
||||
await delay(500)
|
||||
const localContextMenu = await client.locator('.ant-dropdown:not(.ant-dropdown-hidden)')
|
||||
const localHasTerminalAccess = await localContextMenu.locator('.ant-dropdown-menu-item:has-text("Access this folder from the terminal")').count()
|
||||
expect(localHasTerminalAccess).toBe(0)
|
||||
// Close context menu
|
||||
await client.click('.session-current .sftp-panel-title')
|
||||
await delay(500)
|
||||
|
||||
// Access remote folder from terminal
|
||||
await accessFolderFromTerminal(client, 'remote', remoteFolderName)
|
||||
|
||||
// Verify terminal has cd command and folder name
|
||||
const terminalContent = await getTerminalContent(client)
|
||||
expect(terminalContent.includes('cd ')).toBe(true)
|
||||
expect(terminalContent.includes(remoteFolderName)).toBe(true)
|
||||
|
||||
// Clean up - delete both test folders
|
||||
await clickSftpTab(client)
|
||||
await deleteItem(client, 'local', localFolderName)
|
||||
await deleteItem(client, 'remote', remoteFolderName)
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
it('should test rename function for folders in context menu for both local and remote', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Set up SSH connection
|
||||
await setupSftpConnection(client)
|
||||
|
||||
// Test local folder rename
|
||||
const localFolderName = 'test-local-rename-folder-' + Date.now()
|
||||
await createFolder(client, 'local', localFolderName)
|
||||
|
||||
const newLocalFolderName = 'renamed-' + localFolderName
|
||||
await renameItem(client, 'local', localFolderName, newLocalFolderName)
|
||||
|
||||
expect(await verifyFileExists(client, 'local', newLocalFolderName)).toBe(true)
|
||||
|
||||
// Test remote folder rename
|
||||
const remoteFolderName = 'test-remote-rename-folder-' + Date.now()
|
||||
await createFolder(client, 'remote', remoteFolderName)
|
||||
|
||||
const newRemoteFolderName = 'renamed-' + remoteFolderName
|
||||
await renameItem(client, 'remote', remoteFolderName, newRemoteFolderName)
|
||||
|
||||
expect(await verifyFileExists(client, 'remote', newRemoteFolderName)).toBe(true)
|
||||
|
||||
// Clean up - delete the test folders
|
||||
await deleteItem(client, 'local', newLocalFolderName)
|
||||
await deleteItem(client, 'remote', newRemoteFolderName)
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
it('should test enter folder context menu and verify folder content for both remote and local', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Set up SSH connection
|
||||
await setupSftpConnection(client)
|
||||
|
||||
// Test remote file system
|
||||
const remoteFolderName = 'test-remote-enter-folder-' + Date.now()
|
||||
await createFolder(client, 'remote', remoteFolderName)
|
||||
|
||||
// Enter remote folder
|
||||
await enterFolder(client, 'remote', remoteFolderName)
|
||||
|
||||
// Verify remote folder content
|
||||
expect(await verifyCurrentPath(client, 'remote', remoteFolderName)).toBe(true)
|
||||
expect(await countFileListItems(client, 'remote', '.sftp-item')).toBe(2)
|
||||
expect(await countFileListItems(client, 'remote', '.parent-file-item')).toBe(1)
|
||||
|
||||
// Go back to remote parent directory
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
expect(await verifyCurrentPath(client, 'remote', remoteFolderName)).toBe(false)
|
||||
|
||||
// Test local file system
|
||||
const localFolderName = 'test-local-enter-folder-' + Date.now()
|
||||
await createFolder(client, 'local', localFolderName)
|
||||
|
||||
// Enter local folder
|
||||
await enterFolder(client, 'local', localFolderName)
|
||||
|
||||
// Verify local folder content
|
||||
expect(await verifyCurrentPath(client, 'local', localFolderName)).toBe(true)
|
||||
expect(await countFileListItems(client, 'local', '.sftp-item')).toBe(2)
|
||||
expect(await countFileListItems(client, 'local', '.parent-file-item')).toBe(1)
|
||||
|
||||
// Go back to local parent directory
|
||||
await navigateToParentFolder(client, 'local')
|
||||
expect(await verifyCurrentPath(client, 'local', localFolderName)).toBe(false)
|
||||
|
||||
// Clean up - delete the test folders
|
||||
await deleteItem(client, 'remote', remoteFolderName)
|
||||
await deleteItem(client, 'local', localFolderName)
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteItem,
|
||||
copyItem,
|
||||
pasteItem,
|
||||
enterFolder,
|
||||
navigateToParentFolder
|
||||
} = require('./common/common')
|
||||
|
||||
describe('file-copy-paste-operation', function () {
|
||||
it('should test file copy and paste operations', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
await setupSftpConnection(client)
|
||||
|
||||
// Test for both local and remote
|
||||
await testCopyPasteOperation(client, 'local')
|
||||
await testCopyPasteOperation(client, 'remote')
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
})
|
||||
|
||||
async function testCopyPasteOperation (client, type) {
|
||||
// Create a main test folder to contain all test operations
|
||||
const mainTestFolderName = `test-copy-paste-${Date.now()}`
|
||||
await createFolder(client, type, mainTestFolderName)
|
||||
|
||||
// Enter the main test folder
|
||||
await enterFolder(client, type, mainTestFolderName)
|
||||
|
||||
// Create a test file
|
||||
const fileName = `original-file-${Date.now()}.js`
|
||||
await createFile(client, type, fileName)
|
||||
|
||||
// Copy the file
|
||||
await copyItem(client, type, fileName)
|
||||
|
||||
// Give more time for the clipboard to update
|
||||
await delay(2000)
|
||||
|
||||
// Test 1: Paste in the same directory
|
||||
await pasteItem(client, type)
|
||||
|
||||
// Verify that a renamed file was created
|
||||
const renamedFiles = await client.locator(`.session-current .file-list.${type} .sftp-item[title*="${fileName.slice(0, -3)}("]`)
|
||||
const count = await renamedFiles.count()
|
||||
expect(count).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// Verify the renamed file follows the pattern
|
||||
const renamedFileName = await renamedFiles.first().getAttribute('title')
|
||||
expect(renamedFileName).toMatch(/^original-file-\d+\([\w\d-]+\)\.js$/)
|
||||
|
||||
// Test 2: Create folder, paste into subfolder
|
||||
// Create a subfolder
|
||||
const subFolderName = `sub-folder-${Date.now()}`
|
||||
await createFolder(client, type, subFolderName)
|
||||
|
||||
// Copy the original file again
|
||||
await copyItem(client, type, fileName)
|
||||
|
||||
// Give more time for the clipboard to update
|
||||
await delay(2000)
|
||||
|
||||
// Enter the subfolder
|
||||
await enterFolder(client, type, subFolderName)
|
||||
|
||||
// Paste the file in the subfolder
|
||||
await pasteItem(client, type)
|
||||
|
||||
// Give time for the paste to complete
|
||||
await delay(2000)
|
||||
|
||||
// Verify the file was created in the subfolder
|
||||
const copiedFiles = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${fileName}"]`)
|
||||
const copiedCount = await copiedFiles.count()
|
||||
expect(copiedCount).toBe(1)
|
||||
|
||||
// Navigate back to main test folder
|
||||
await navigateToParentFolder(client, type)
|
||||
|
||||
// Navigate back to the parent folder (outside the main test folder)
|
||||
await navigateToParentFolder(client, type)
|
||||
|
||||
// Clean up - delete the entire main test folder with all its contents
|
||||
await deleteItem(client, type, mainTestFolderName)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFile,
|
||||
createFolder,
|
||||
enterFolder,
|
||||
copyItem,
|
||||
pasteItem,
|
||||
deleteItem
|
||||
} = require('./common/common')
|
||||
|
||||
describe('file-copy-paste-operation-keyboard', function () {
|
||||
it('should test file copy and paste operations using keyboard shortcuts', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
await setupSftpConnection(client)
|
||||
|
||||
// Test for both local and remote
|
||||
await testCopyPasteOperationWithKeyboard(client, 'local')
|
||||
await testCopyPasteOperationWithKeyboard(client, 'remote')
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
})
|
||||
|
||||
async function testCopyPasteOperationWithKeyboard (client, type) {
|
||||
// Create a main test folder to contain all test operations
|
||||
const mainTestFolderName = `test-keyboard-copy-paste-${Date.now()}`
|
||||
await createFolder(client, type, mainTestFolderName)
|
||||
|
||||
// Enter the main test folder
|
||||
await enterFolder(client, type, mainTestFolderName)
|
||||
|
||||
// Create a test file
|
||||
const fileName = `keyboard-copy-file-${Date.now()}.js`
|
||||
await createFile(client, type, fileName)
|
||||
|
||||
// Copy the file using keyboard shortcut - only need to copy once
|
||||
await copyItem(client, type, fileName)
|
||||
|
||||
// Give time for the clipboard to update
|
||||
await delay(2000)
|
||||
|
||||
// Test 1: Paste in the same directory using keyboard shortcut
|
||||
await pasteItem(client, type)
|
||||
|
||||
// Verify that a renamed file was created
|
||||
const renamedFiles = await client.locator(`.session-current .file-list.${type} .sftp-item[title*="${fileName.slice(0, -3)}("]`)
|
||||
const count = await renamedFiles.count()
|
||||
expect(count).toBeGreaterThanOrEqual(1)
|
||||
|
||||
// Verify the renamed file follows the pattern
|
||||
const renamedFileName = await renamedFiles.first().getAttribute('title')
|
||||
expect(renamedFileName).toMatch(/^keyboard-copy-file-\d+\([\w\d-]+\)\.js$/)
|
||||
|
||||
// Test 2: Create folder, paste into subfolder using keyboard shortcuts
|
||||
const subFolderName = `keyboard-sub-folder-${Date.now()}`
|
||||
await createFolder(client, type, subFolderName)
|
||||
|
||||
// Enter the subfolder
|
||||
await enterFolder(client, type, subFolderName)
|
||||
|
||||
// Paste the file in the subfolder using keyboard shortcut
|
||||
await pasteItem(client, type)
|
||||
|
||||
// Give time for the paste to complete
|
||||
await delay(2000)
|
||||
|
||||
// Verify the file was created in the subfolder
|
||||
const copiedFiles = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${fileName}"]`)
|
||||
const copiedCount = await copiedFiles.count()
|
||||
expect(copiedCount).toBe(1)
|
||||
|
||||
// Navigate back to main test folder
|
||||
await client.doubleClick(`.session-current .file-list.${type} .parent-file-item`)
|
||||
await delay(3000)
|
||||
|
||||
// Navigate back to the parent folder (outside the main test folder)
|
||||
await client.doubleClick(`.session-current .file-list.${type} .parent-file-item`)
|
||||
await delay(3000)
|
||||
|
||||
// Clean up - delete the entire main test folder with all its contents
|
||||
await deleteItem(client, type, mainTestFolderName)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const { expect } = require('./common/expect')
|
||||
const appOptions = require('./common/app-options')
|
||||
const e = require('./common/lang')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('terminal themes', function () {
|
||||
it('all buttons open proper terminal themes tab', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
log('button:edit')
|
||||
await client.click('.btns .anticon-picture')
|
||||
await delay(500)
|
||||
const sel = '.setting-wrap .ant-tabs-nav-list .ant-tabs-tab-active'
|
||||
await client.hasElem(sel)
|
||||
await delay(500)
|
||||
const text = await client.getText(sel)
|
||||
expect(text).equal(e('uiThemes'))
|
||||
|
||||
const v = await client.getValue('.setting-wrap #terminal-theme-form_themeName')
|
||||
const tx = await client.getText('.setting-wrap .item-list-unit.active')
|
||||
const txd = await client.getText('.setting-wrap .item-list-unit.current')
|
||||
expect(v).equal(e('newTheme'))
|
||||
expect(tx).equal(e('newTheme'))
|
||||
expect(txd).equal(e('default'))
|
||||
|
||||
// create theme
|
||||
log('create theme')
|
||||
const themePrev = await client.evaluate(() => {
|
||||
return window.store.terminalThemes.length
|
||||
})
|
||||
const themeIterm = await client.evaluate(() => {
|
||||
return window.store.itermThemes.length
|
||||
})
|
||||
await client.click('.setting-wrap .ant-btn-primary')
|
||||
|
||||
const themeNow = await client.evaluate(() => {
|
||||
return window.store.terminalThemes.length
|
||||
})
|
||||
await delay(1000)
|
||||
expect(themeNow).equal(themePrev + 1)
|
||||
expect(themeIterm > 10).equal(true)
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,169 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it, expect } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const nanoid = require('./common/uid')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { setupSftpConnection } = require('./common/common')
|
||||
|
||||
describe('file info modal', function () {
|
||||
it('should open window and basic file info modal works for both local and remote', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Create SSH connection
|
||||
await setupSftpConnection(client)
|
||||
|
||||
// Test local file info modal
|
||||
await testFileInfoModal(client, 'local', 'click')
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
|
||||
it('should test edit permission functionality for both local and remote files', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// Create SSH connection
|
||||
await setupSftpConnection(client)
|
||||
|
||||
// Test local file edit permission
|
||||
await testEditFolderPermission(client, 'local')
|
||||
|
||||
// Test remote file edit permission
|
||||
await testEditFolderPermission(client, 'remote')
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
|
||||
async function testEditFolderPermission (client, folderType) {
|
||||
const folderName = `${folderType}-test-folder-${nanoid()}`
|
||||
|
||||
// Create a new folder
|
||||
await client.rightClick(`.session-current .file-list.${folderType} .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(200)
|
||||
await client.setValue('.session-current .sftp-item input', folderName)
|
||||
await client.click('.session-current .sftp-panel-title')
|
||||
await delay(2500)
|
||||
|
||||
// Right-click on the folder and select "Edit Permission"
|
||||
await client.rightClick(`.session-current .file-list.${folderType} .sftp-item[title="${folderName}"]`, 10, 10)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Edit Permission")')
|
||||
await delay(1000)
|
||||
|
||||
// Verify that the edit permission modal is open
|
||||
await client.hasElem('.custom-modal-container')
|
||||
|
||||
// Check if the modal title is "Edit Folder Permission"
|
||||
const modalTitle = await client.getText('.custom-modal-title')
|
||||
expect(modalTitle).toBe('Edit Folder Permission')
|
||||
|
||||
// Change a specific permission (e.g., 'other' 'write')
|
||||
const targetGroup = 'other'
|
||||
const targetPermission = 'write'
|
||||
const buttonSelector = `.custom-modal-container .pd1b:has-text("${targetGroup}") .ant-btn:has-text("${targetPermission}")`
|
||||
|
||||
const initialClass = await client.getAttribute(buttonSelector, 'class')
|
||||
const initiallyActive = initialClass.includes('ant-btn-primary')
|
||||
|
||||
await client.click(buttonSelector)
|
||||
await delay(200)
|
||||
|
||||
const newClass = await client.getAttribute(buttonSelector, 'class')
|
||||
const nowActive = newClass.includes('ant-btn-primary')
|
||||
|
||||
console.log(`${targetGroup} ${targetPermission}: Initial: ${initiallyActive}, Now: ${nowActive}`)
|
||||
expect(nowActive).not.toBe(initiallyActive)
|
||||
|
||||
// Save the changes
|
||||
await client.click('.custom-modal-footer .ant-btn-primary')
|
||||
await delay(2000)
|
||||
|
||||
// Verify that the modal is closed
|
||||
await client.hasElem('.custom-modal-container', false)
|
||||
|
||||
// Open folder properties to check if permissions were updated
|
||||
await client.rightClick(`.session-current .file-list.${folderType} .sftp-item[title="${folderName}"]`, 10, 10)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .anticon-info-circle')
|
||||
await delay(1200)
|
||||
|
||||
// Verify that the specific permission was updated in the folder properties
|
||||
const infoButtonClass = await client.getAttribute(buttonSelector, 'class')
|
||||
const infoButtonActive = infoButtonClass.includes('ant-btn-primary')
|
||||
expect(infoButtonActive).toBe(nowActive)
|
||||
|
||||
// Close the folder properties modal
|
||||
await client.click('.custom-modal-close')
|
||||
await delay(300)
|
||||
|
||||
// Clean up - delete the test folder
|
||||
await client.click(`.session-current .file-list.${folderType} .sftp-item[title="${folderName}"]`)
|
||||
await delay(400)
|
||||
await client.keyboard.press('Delete')
|
||||
await delay(400)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(2500)
|
||||
|
||||
// Verify folder is deleted
|
||||
await client.hasElem(`.session-current .file-list.${folderType} .sftp-item[title="${folderName}"]`, false)
|
||||
}
|
||||
|
||||
async function testFileInfoModal (client, fileType, closeMethod) {
|
||||
const fname = `${fileType}-test-electerm-${nanoid()}`
|
||||
|
||||
// Create a new folder
|
||||
await client.rightClick(`.session-current .file-list.${fileType} .real-file-item`, 10, 10)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .anticon-folder-add')
|
||||
await delay(200)
|
||||
await client.setValue('.session-current .sftp-item input', fname)
|
||||
await client.click('.session-current .sftp-panel-title')
|
||||
await delay(2500)
|
||||
|
||||
// Verify folder was created
|
||||
await client.hasElem(`.session-current .file-list.${fileType} .sftp-item[title="${fname}"]`)
|
||||
|
||||
// Open info modal
|
||||
await client.rightClick(`.session-current .file-list.${fileType} .sftp-item[title="${fname}"]`, 10, 10)
|
||||
await delay(200)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .anticon-info-circle')
|
||||
await delay(1200)
|
||||
|
||||
// Verify modal content and visibility
|
||||
await client.hasElem('.custom-modal-container')
|
||||
await client.hasElem('.custom-modal-container')
|
||||
await client.hasElem('.custom-modal-wrap .file-props')
|
||||
|
||||
// Close modal using different methods
|
||||
if (closeMethod === 'click') {
|
||||
await client.click('.custom-modal-close')
|
||||
} else {
|
||||
await client.keyboard.press('Escape')
|
||||
}
|
||||
await delay(300)
|
||||
|
||||
// Verify modal is closed
|
||||
await client.hasElem('.custom-modal-container', false)
|
||||
|
||||
// Delete the test folder
|
||||
await client.click(`.session-current .file-list.${fileType} .sftp-item[title="${fname}"]`)
|
||||
await delay(400)
|
||||
await client.keyboard.press('Delete')
|
||||
await delay(400)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(2500)
|
||||
|
||||
// Verify folder is deleted
|
||||
await client.hasElem(`.session-current .file-list.${fileType} .sftp-item[title="${fname}"]`, false)
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* timeout setting
|
||||
* need TEST_HOST TEST_PASS TEST_USER env set
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { closeApp, setupSshConnection } = require('./common/common')
|
||||
|
||||
describe('timeout setting', function () {
|
||||
it('timeout setting works', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(6000)
|
||||
|
||||
log('0101.timeout-setting.spec.js: set timeout to 100')
|
||||
await client.evaluate(() => {
|
||||
window.store.setConfig({
|
||||
sshReadyTimeout: 100
|
||||
})
|
||||
})
|
||||
await delay(400)
|
||||
|
||||
const timeout = await client.evaluate(() => {
|
||||
return window.store.config.sshReadyTimeout
|
||||
})
|
||||
await delay(150)
|
||||
expect(timeout).equal(100)
|
||||
|
||||
log('0101.timeout-setting.spec.js: open new ssh and timeout')
|
||||
await setupSshConnection(client)
|
||||
log('0101.timeout-setting.spec.js: ssh form submitted')
|
||||
await delay(5500)
|
||||
log('0101.timeout-setting.spec.js: waited after submit')
|
||||
const errSel = '.terminal-error-handle .ant-alert-title'
|
||||
log('0101.timeout-setting.spec.js: starting error check loop')
|
||||
for (let i = 0; i < 25; i++) {
|
||||
await delay(500)
|
||||
const errExist = await client.elemExist(errSel)
|
||||
if (errExist) {
|
||||
log('0101.timeout-setting.spec.js: error found at iteration ' + i)
|
||||
break
|
||||
}
|
||||
}
|
||||
const txt = await client.getText(errSel)
|
||||
log('0101.timeout-setting.spec.js: error text: ' + txt)
|
||||
expect(txt.includes('Timed out')).equal(true)
|
||||
|
||||
log('0101.timeout-setting.spec.js: set timeout to 50000')
|
||||
await delay(1500)
|
||||
await client.evaluate(() => {
|
||||
window.store.setConfig({
|
||||
sshReadyTimeout: 50000
|
||||
})
|
||||
})
|
||||
await delay(1555)
|
||||
const timeout1 = await client.evaluate(() => {
|
||||
return window.store.config.sshReadyTimeout
|
||||
})
|
||||
await delay(150)
|
||||
expect(timeout1).equal(50000)
|
||||
log('0101.timeout-setting.spec.js: timeout set to 50000 verified')
|
||||
await delay(400)
|
||||
log('0101.timeout-setting.spec.js: closing app')
|
||||
await closeApp(electronApp, __filename)
|
||||
log('0101.timeout-setting.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,187 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const { expect } = require('./common/expect')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { spawn } = require('child_process')
|
||||
const path = require('path')
|
||||
|
||||
describe('AI Create Bookmark', function () {
|
||||
let aiServer
|
||||
|
||||
it.beforeAll(async () => {
|
||||
const serverPath = path.join(__dirname, 'common', 'ai-api.js')
|
||||
aiServer = spawn('node', [serverPath])
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
})
|
||||
|
||||
it.afterAll(() => {
|
||||
if (aiServer) {
|
||||
aiServer.kill()
|
||||
}
|
||||
})
|
||||
|
||||
it('should configure AI and create bookmark using AI', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(5500)
|
||||
|
||||
log('clear AI config to trigger AI config page')
|
||||
await client.evaluate(() => {
|
||||
return window.store.setConfig({
|
||||
modelAI: ''
|
||||
})
|
||||
})
|
||||
|
||||
log('open bookmarks page')
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(2500)
|
||||
|
||||
const sel = '.setting-wrap .ant-tabs-nav-list .ant-tabs-tab-active'
|
||||
await client.hasElem(sel)
|
||||
const text = await client.getText(sel)
|
||||
expect(text).equal('Bookmarks')
|
||||
|
||||
log('click AI create bookmark button - should open AI config')
|
||||
const aiCreateBtn = client.locator('.create-ai-btn')
|
||||
await aiCreateBtn.click()
|
||||
await delay(1000)
|
||||
|
||||
log('verify AI config form is shown')
|
||||
await client.hasElem('.ai-config-modal .ai-config-form')
|
||||
|
||||
log('fill AI configuration form')
|
||||
await client.setValue('#baseURLAI', 'http://localhost:43434')
|
||||
await client.setValue('#apiPathAI', '/chat/completions')
|
||||
await client.setValue('#modelAI', 'gpt-3.5-turbo')
|
||||
await client.setValue('#apiKeyAI', 'test-api-key')
|
||||
await client.setValue('#roleAI', 'You are a helpful assistant')
|
||||
await client.setValue('#languageAI', 'English')
|
||||
|
||||
log('save AI configuration')
|
||||
await client.click('.ai-config-form button[type="submit"]')
|
||||
await delay(1000)
|
||||
|
||||
// log('close setting panel')
|
||||
// await client.click('.custom-modal-close')
|
||||
// await delay(1000)
|
||||
|
||||
log('open bookmarks page again')
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(2500)
|
||||
|
||||
log('click AI create bookmark button again')
|
||||
const aiCreateBtn2 = client.locator('.create-ai-btn')
|
||||
await aiCreateBtn2.click()
|
||||
await delay(1000)
|
||||
|
||||
log('verify AI bookmark form is shown')
|
||||
await client.hasElem('.ai-bookmark-form')
|
||||
|
||||
log('enter description for bookmark')
|
||||
const testDescription = 'A test SSH server for development'
|
||||
await client.setValue('.ai-bookmark-form textarea', testDescription)
|
||||
|
||||
log('click generate button')
|
||||
const generateBtn = client.locator('.ai-bookmark-form button:has-text("Submit")')
|
||||
await generateBtn.click()
|
||||
await delay(1500)
|
||||
|
||||
log('verify modal and JSON preview')
|
||||
await client.hasElem('.custom-modal-wrap')
|
||||
|
||||
const jsonPreview = client.locator('.ai-bookmark-json-preview')
|
||||
await client.hasElem('.ai-bookmark-json-preview')
|
||||
const previewText = await jsonPreview.textContent()
|
||||
expect(previewText).toContain('Test Server')
|
||||
expect(previewText).toContain('test.example.com')
|
||||
|
||||
log('toggle to edit mode')
|
||||
const editBtn = client.locator('.ai-action-buttons .anticon-edit')
|
||||
await editBtn.click()
|
||||
await delay(500)
|
||||
|
||||
log('verify editor is shown')
|
||||
await client.hasElem('.simple-editor')
|
||||
|
||||
log('edit title and port in editor')
|
||||
const timestamp = Date.now()
|
||||
const editor = client.locator('.simple-editor textarea')
|
||||
const editorContent = await editor.inputValue()
|
||||
const updatedContent = editorContent
|
||||
.replace('"title": "Test Server"', `"title": "Test Server ${timestamp}"`)
|
||||
.replace('"port": 22', '"port": 23')
|
||||
await editor.fill(updatedContent)
|
||||
await delay(500)
|
||||
|
||||
log('toggle back to preview mode')
|
||||
const previewBtn = client.locator('.ai-action-buttons .anticon-eye')
|
||||
await previewBtn.click()
|
||||
await delay(500)
|
||||
|
||||
log('verify preview shows updated port')
|
||||
const updatedPreview = client.locator('.ai-bookmark-json-preview')
|
||||
const updatedPreviewText = await updatedPreview.textContent()
|
||||
expect(updatedPreviewText).toContain('"port": 23')
|
||||
expect(updatedPreviewText).toContain(`"title": "Test Server ${timestamp}"`)
|
||||
|
||||
log('test copy button')
|
||||
const copyBtn = client.locator('.ai-action-buttons .anticon-copy')
|
||||
await copyBtn.click()
|
||||
await delay(500)
|
||||
|
||||
log('verify clipboard content')
|
||||
const clipboardContent = await client.readClipboard()
|
||||
expect(clipboardContent).toContain('"port": 23')
|
||||
|
||||
log('confirm bookmark creation')
|
||||
const confirmBtn = client.locator('.custom-modal-wrap .custom-modal-footer-buttons button:has-text("Confirm")')
|
||||
await confirmBtn.click()
|
||||
await delay(1000)
|
||||
|
||||
log('verify bookmark was created with updated port')
|
||||
const bookmarks = await client.evaluate(() => {
|
||||
return window.store.bookmarks
|
||||
})
|
||||
const createdBookmark = bookmarks.find(b =>
|
||||
b.title && b.title.includes(timestamp.toString())
|
||||
)
|
||||
expect(createdBookmark).toBeDefined()
|
||||
expect(createdBookmark.title).toContain(timestamp.toString())
|
||||
expect(createdBookmark.host).toEqual('test.example.com')
|
||||
expect(createdBookmark.port).toEqual(23)
|
||||
expect(createdBookmark.username).toEqual('testuser')
|
||||
expect(createdBookmark.type).toEqual('ssh')
|
||||
expect(createdBookmark.enableSsh).toEqual(true)
|
||||
expect(createdBookmark.enableSftp).toEqual(true)
|
||||
expect(createdBookmark.useSshAgent).toEqual(true)
|
||||
expect(createdBookmark.x11).toEqual(false)
|
||||
expect(createdBookmark.term).toEqual('xterm-256color')
|
||||
expect(createdBookmark.displayRaw).toEqual(false)
|
||||
expect(createdBookmark.authType).toEqual('password')
|
||||
expect(createdBookmark.encode).toEqual('utf8')
|
||||
expect(createdBookmark.envLang).toEqual('en_US.UTF-8')
|
||||
|
||||
await delay(500)
|
||||
|
||||
log('verify AI bookmark form is shown')
|
||||
await client.hasElem('.ai-bookmark-form')
|
||||
|
||||
log('click cancel button to return to normal form')
|
||||
const cancelBtn = client.locator('.ai-bookmark-form .ant-btn .anticon-close').first()
|
||||
await cancelBtn.click()
|
||||
await delay(1500)
|
||||
|
||||
log('verify returned to normal bookmark edit form')
|
||||
await client.hasElem('.form-wrap #ssh-form_host')
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,250 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(1000000)
|
||||
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFile,
|
||||
createFolder,
|
||||
enterFolder,
|
||||
navigateToParentFolder,
|
||||
deleteItem,
|
||||
selectAllContextMenu,
|
||||
verifyFileTransfersComplete,
|
||||
closeApp
|
||||
} = require('./common/common')
|
||||
|
||||
describe('file-transfer-conflict-resolution', function () {
|
||||
it('should handle bidirectional file transfer conflicts with different resolution policies', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
log('018.file-transfer-conflict.spec.js: app launched')
|
||||
|
||||
await setupSftpConnection(client)
|
||||
log('018.file-transfer-conflict.spec.js: sftp connected')
|
||||
await delay(2000)
|
||||
|
||||
// Create a single test folder structure for all tests
|
||||
const timestamp = Date.now()
|
||||
const testFolder = `conflict-test-${timestamp}`
|
||||
|
||||
try {
|
||||
// Create and prepare test environment
|
||||
await prepareTestEnvironment(client, testFolder)
|
||||
log('018.file-transfer-conflict.spec.js: test environment prepared')
|
||||
|
||||
// Test conflict policies in both directions
|
||||
await testAllConflictPolicies(client, testFolder)
|
||||
log('018.file-transfer-conflict.spec.js: conflict policies tested')
|
||||
} finally {
|
||||
// Clean up test folders once at the end
|
||||
await cleanupTestFolders(client, testFolder)
|
||||
log('018.file-transfer-conflict.spec.js: test folders cleaned')
|
||||
}
|
||||
|
||||
await closeApp(electronApp, __filename)
|
||||
log('018.file-transfer-conflict.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
|
||||
async function prepareTestEnvironment (client, testFolder) {
|
||||
// Create main test folder in both locations
|
||||
await createFolder(client, 'local', testFolder)
|
||||
await delay(1000)
|
||||
await createFolder(client, 'remote', testFolder)
|
||||
await delay(1000)
|
||||
|
||||
// Enter both folders
|
||||
await enterFolder(client, 'local', testFolder)
|
||||
await delay(1000)
|
||||
await enterFolder(client, 'remote', testFolder)
|
||||
await delay(1000)
|
||||
|
||||
// Create test files and folders in local only
|
||||
const testFiles = [
|
||||
'test-file-1.txt',
|
||||
'test-file-2.txt',
|
||||
'test-file-3.txt'
|
||||
]
|
||||
|
||||
const testFolders = [
|
||||
'test-folder-1',
|
||||
'test-folder-2',
|
||||
'test-folder-3'
|
||||
]
|
||||
|
||||
// Create files in local
|
||||
for (const fileName of testFiles) {
|
||||
await createFile(client, 'local', fileName)
|
||||
await delay(500)
|
||||
}
|
||||
|
||||
// Create folders in local
|
||||
for (const folderName of testFolders) {
|
||||
await createFolder(client, 'local', folderName)
|
||||
await delay(500)
|
||||
}
|
||||
|
||||
// Now upload everything to the remote to create identical structure
|
||||
await uploadAllToRemote(client)
|
||||
}
|
||||
|
||||
async function uploadAllToRemote (client) {
|
||||
// Select all local items
|
||||
await selectAllContextMenu(client, 'local')
|
||||
await delay(1000)
|
||||
|
||||
// Upload to remote
|
||||
await client.rightClick('.session-current .file-list.local .sftp-item.selected', 10, 10)
|
||||
await delay(1000)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item .anticon.anticon-cloud-upload')
|
||||
await delay(3000)
|
||||
|
||||
// Wait for transfers to complete
|
||||
await verifyFileTransfersComplete(client)
|
||||
await delay(1000)
|
||||
}
|
||||
|
||||
async function testAllConflictPolicies (client, testFolder) {
|
||||
// Test each policy for local to remote transfers
|
||||
await testConflictResolution(client, 'overwrite', 'local', 'remote')
|
||||
await testConflictResolution(client, 'rename', 'local', 'remote')
|
||||
await testConflictResolution(client, 'skip', 'local', 'remote')
|
||||
|
||||
// Test each policy for remote to local transfers
|
||||
// await testConflictResolution(client, 'overwrite', 'remote', 'local')
|
||||
// await testConflictResolution(client, 'rename', 'remote', 'local')
|
||||
// await testConflictResolution(client, 'skip', 'remote', 'local')
|
||||
}
|
||||
|
||||
async function testConflictResolution (client, policy, fromType, toType) {
|
||||
// Select all items in the source panel
|
||||
const selectedItems = await selectAllItems(client, fromType)
|
||||
await delay(1500)
|
||||
|
||||
// Initiate transfer
|
||||
const isUpload = fromType === 'local'
|
||||
await client.rightClick(`.session-current .file-list.${fromType} .sftp-item.selected`, 10, 10)
|
||||
await delay(1500)
|
||||
|
||||
// Click appropriate transfer menu item
|
||||
const menuIconClass = isUpload ? 'cloud-upload' : 'cloud-download'
|
||||
await client.click(`.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item .anticon.anticon-${menuIconClass}`)
|
||||
await delay(3000)
|
||||
|
||||
// Handle conflict resolution based on policy
|
||||
if (policy === 'skip') {
|
||||
await client.click('.custom-modal-footer button:has-text("Skip all")')
|
||||
// Debug: Check if modal closes after click
|
||||
await delay(1000)
|
||||
if (await client.elemExist('.custom-modal-container')) {
|
||||
await client.click('.custom-modal-footer button:has-text("Skip all")')
|
||||
}
|
||||
} else if (policy === 'overwrite') {
|
||||
// Check for first conflict item type (file vs folder) and click appropriate button
|
||||
const isFolderConflict = await client.elemExist('.custom-modal-footer button:has-text("Merge all")')
|
||||
if (isFolderConflict) {
|
||||
await client.click('.custom-modal-footer button:has-text("Merge all")')
|
||||
} else {
|
||||
await client.click('.custom-modal-footer button:has-text("Overwrite all")')
|
||||
}
|
||||
} else if (policy === 'rename') {
|
||||
await client.click('.custom-modal-footer button:has-text("Rename all")')
|
||||
} else {
|
||||
throw new Error(`Unsupported policy: ${policy}`)
|
||||
}
|
||||
|
||||
await delay(5000)
|
||||
|
||||
// Wait for transfers to complete for overwrite and rename
|
||||
if (policy !== 'skip') {
|
||||
await verifyFileTransfersComplete(client)
|
||||
} else {
|
||||
// For skip, we've already handled each conflict manually
|
||||
await delay(2000)
|
||||
}
|
||||
|
||||
// Verify results based on policy
|
||||
if (policy === 'rename') {
|
||||
// Verify renamed items exist in destination (with rename- pattern)
|
||||
const renamedItems = await client.locator(`.session-current .file-list.${toType} .sftp-item[title*="(rename-"]`)
|
||||
const count = await renamedItems.count()
|
||||
expect(count).toBeGreaterThan(0, `Expected to find renamed items from ${fromType} to ${toType} with policy ${policy}`)
|
||||
} else if (policy === 'overwrite') {
|
||||
// For overwrite, just verify the original count of items is maintained
|
||||
const items = await client.locator(`.session-current .file-list.${toType} .real-file-item`)
|
||||
const count = await items.count()
|
||||
expect(count).toBe(selectedItems, `Expected to find ${selectedItems} items in ${toType} after overwrite`)
|
||||
}
|
||||
}
|
||||
|
||||
async function selectAllItems (client, type) {
|
||||
// First deselect everything by clicking empty space
|
||||
await client.click(`.session-current .file-list.${type} .sftp-item`)
|
||||
await delay(500)
|
||||
|
||||
// Use selection context menu to select all real file items
|
||||
await selectAllContextMenu(client, type)
|
||||
await delay(1000)
|
||||
|
||||
// Count and return the number of selected items
|
||||
const selectedItems = await client.locator(`.session-current .file-list.${type} .sftp-item.selected`).count()
|
||||
expect(selectedItems).toBeGreaterThan(0, `Expected to select items in ${type}`)
|
||||
|
||||
return selectedItems
|
||||
}
|
||||
|
||||
// async function handleSkipForEachItem (client, expectedItemCount) {
|
||||
// // For folders with files inside, we may have more conflicts than the base item count
|
||||
// // To handle this, we'll track the current conflict index and click Skip until all are done
|
||||
// let conflictsHandled = 0
|
||||
// // let timeWithoutConflict = 0
|
||||
// const waitInterval = 2000 // Time to wait between checks
|
||||
// // Continue until we have no more conflicts for a reasonable time
|
||||
// while (conflictsHandled < expectedItemCount) {
|
||||
// await client.click('.custom-modal-footer button span:text-is("Skip")')
|
||||
// conflictsHandled++
|
||||
|
||||
// // Wait for a short time before checking again
|
||||
// await delay(waitInterval)
|
||||
// }
|
||||
|
||||
// console.log(`Total conflicts skipped: ${conflictsHandled}`)
|
||||
|
||||
// // We should have at least as many conflicts as selected items
|
||||
// expect(conflictsHandled).toBeGreaterThanOrEqual(expectedItemCount,
|
||||
// `Expected at least ${expectedItemCount} conflicts to be skipped, but only skipped ${conflictsHandled}`)
|
||||
// }
|
||||
|
||||
async function cleanupTestFolders (client, testFolder) {
|
||||
// Navigate back to parent folders (if not already there)
|
||||
// Check if we need to navigate back
|
||||
const localPathInput = await client.getValue('.session-current .sftp-local-section .sftp-title input')
|
||||
if (localPathInput.includes(testFolder)) {
|
||||
await navigateToParentFolder(client, 'local')
|
||||
await delay(1000)
|
||||
}
|
||||
|
||||
const remotePathInput = await client.getValue('.session-current .sftp-remote-section .sftp-title input')
|
||||
if (remotePathInput.includes(testFolder)) {
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
await delay(1000)
|
||||
}
|
||||
|
||||
// Delete test folders
|
||||
await deleteItem(client, 'local', testFolder)
|
||||
await delay(1000)
|
||||
await deleteItem(client, 'remote', testFolder)
|
||||
await delay(1000)
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteItem,
|
||||
enterFolder,
|
||||
navigateToParentFolder,
|
||||
verifyFileExists,
|
||||
verifyFileTransfersComplete,
|
||||
selectAllContextMenu, // Added missing import
|
||||
closeApp
|
||||
} = require('./common/common')
|
||||
|
||||
describe('file-transfer-local-remote', function () {
|
||||
it('should test file and folder transfer including folders with files', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
log('018.file-transfer.spec.js: app launched')
|
||||
|
||||
await setupSftpConnection(client)
|
||||
log('018.file-transfer.spec.js: sftp connected')
|
||||
|
||||
// Test transfer local->remote and remote->local
|
||||
await testFileTransfer(client)
|
||||
log('018.file-transfer.spec.js: file transfer tested')
|
||||
|
||||
await closeApp(electronApp, __filename)
|
||||
log('018.file-transfer.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
|
||||
async function testFileTransfer (client) {
|
||||
const timestamp = Date.now()
|
||||
|
||||
// Create main test folders in both local and remote
|
||||
const mainFolder = `transfer-test-${timestamp}`
|
||||
|
||||
await createFolder(client, 'local', mainFolder)
|
||||
await createFolder(client, 'remote', mainFolder)
|
||||
|
||||
// Enter the test folder in local
|
||||
await enterFolder(client, 'local', mainFolder)
|
||||
await delay(2000)
|
||||
|
||||
// Create test files in local with specific prefixes for easier identification
|
||||
const localFiles = [
|
||||
`local-file-1-${timestamp}.txt`,
|
||||
`local-file-2-${timestamp}.txt`,
|
||||
`local-file-3-${timestamp}.txt`
|
||||
]
|
||||
|
||||
// Create test folders in local
|
||||
const localFolders = [
|
||||
`local-folder-1-${timestamp}`,
|
||||
`local-folder-2-${timestamp}`,
|
||||
`local-folder-3-${timestamp}`
|
||||
]
|
||||
|
||||
// Create all test items in local
|
||||
for (const fileName of localFiles) {
|
||||
await createFile(client, 'local', fileName)
|
||||
await delay(800)
|
||||
}
|
||||
|
||||
for (const folderName of localFolders) {
|
||||
await createFolder(client, 'local', folderName)
|
||||
await delay(800)
|
||||
}
|
||||
|
||||
// Drag a file into one of the folders in local
|
||||
const localSourceElement = await client.locator(`.session-current .file-list.local .sftp-item[title="${localFiles[0]}"]`)
|
||||
const localTargetElement = await client.locator(`.session-current .file-list.local .sftp-item[title="${localFolders[0]}"]`)
|
||||
|
||||
// Get bounding boxes for drag operation
|
||||
const localSourceBound = await localSourceElement.boundingBox()
|
||||
const localTargetBound = await localTargetElement.boundingBox()
|
||||
|
||||
// Perform drag operation
|
||||
await client.mouse.move(localSourceBound.x + localSourceBound.width / 2, localSourceBound.y + localSourceBound.height / 2)
|
||||
await client.mouse.down()
|
||||
await delay(500)
|
||||
await client.mouse.move(localTargetBound.x + localTargetBound.width / 2, localTargetBound.y + localTargetBound.height / 2, { steps: 20 })
|
||||
await delay(500)
|
||||
await client.mouse.up()
|
||||
await delay(3000) // Wait for drag-drop operation to complete
|
||||
|
||||
// Enter the test folder in remote
|
||||
await enterFolder(client, 'remote', mainFolder)
|
||||
await delay(2000)
|
||||
|
||||
// Create test files in remote with specific prefixes
|
||||
const remoteFiles = [
|
||||
`remote-file-1-${timestamp}.txt`,
|
||||
`remote-file-2-${timestamp}.txt`,
|
||||
`remote-file-3-${timestamp}.txt`
|
||||
]
|
||||
|
||||
// Create test folders in remote
|
||||
const remoteFolders = [
|
||||
`remote-folder-1-${timestamp}`,
|
||||
`remote-folder-2-${timestamp}`,
|
||||
`remote-folder-3-${timestamp}`
|
||||
]
|
||||
|
||||
// Create all test items in remote
|
||||
for (const fileName of remoteFiles) {
|
||||
await createFile(client, 'remote', fileName)
|
||||
await delay(800)
|
||||
}
|
||||
|
||||
for (const folderName of remoteFolders) {
|
||||
await createFolder(client, 'remote', folderName)
|
||||
await delay(800)
|
||||
}
|
||||
|
||||
// Drag a file into one of the folders in remote
|
||||
const remoteSourceElement = await client.locator(`.session-current .file-list.remote .sftp-item[title="${remoteFiles[0]}"]`)
|
||||
const remoteTargetElement = await client.locator(`.session-current .file-list.remote .sftp-item[title="${remoteFolders[0]}"]`)
|
||||
|
||||
// Get bounding boxes for drag operation
|
||||
const remoteSourceBound = await remoteSourceElement.boundingBox()
|
||||
const remoteTargetBound = await remoteTargetElement.boundingBox()
|
||||
|
||||
// Perform drag operation
|
||||
await client.mouse.move(remoteSourceBound.x + remoteSourceBound.width / 2, remoteSourceBound.y + remoteSourceBound.height / 2)
|
||||
await client.mouse.down()
|
||||
await delay(500)
|
||||
await client.mouse.move(remoteTargetBound.x + remoteTargetBound.width / 2, remoteTargetBound.y + remoteTargetBound.height / 2, { steps: 20 })
|
||||
await delay(500)
|
||||
await client.mouse.up()
|
||||
await delay(3000) // Wait for drag-drop operation to complete
|
||||
|
||||
// PART 1: LOCAL TO REMOTE TRANSFER (using context menu select all)
|
||||
|
||||
// Since one file is now in a folder, combine remaining visible items
|
||||
const localVisibleItems = [localFiles[1], localFiles[2], ...localFolders]
|
||||
|
||||
// Use context menu to select all visible items in local
|
||||
await selectAllContextMenu(client, 'local')
|
||||
await delay(1000)
|
||||
|
||||
// Verify we selected the correct number of items
|
||||
const selectedLocalItems = await client.locator('.session-current .file-list.local .sftp-item.selected')
|
||||
const selectedLocalCount = await selectedLocalItems.count()
|
||||
expect(selectedLocalCount).toBe(5) // 2 files + 3 folders
|
||||
|
||||
// Use context menu to upload to remote
|
||||
await client.rightClick('.session-current .file-list.local .sftp-item.selected', 10, 10)
|
||||
await delay(1000)
|
||||
|
||||
// Click the upload menu item
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item .anticon.anticon-cloud-upload')
|
||||
await delay(10000) // Increased delay for folders with files to transfer
|
||||
|
||||
// Verify fileTransfers array is empty after the operation
|
||||
await verifyFileTransfersComplete(client)
|
||||
|
||||
// Verify all local items were transferred to remote, including folder structure
|
||||
for (const itemName of localVisibleItems) {
|
||||
expect(await verifyFileExists(client, 'remote', itemName)).toBe(true)
|
||||
}
|
||||
|
||||
// Verify the file inside the transferred folder
|
||||
await enterFolder(client, 'remote', localFolders[0])
|
||||
await delay(2000)
|
||||
expect(await verifyFileExists(client, 'remote', localFiles[0])).toBe(true)
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
await delay(2000)
|
||||
|
||||
// PART 2: REMOTE TO LOCAL TRANSFER (using ctrl/cmd-select)
|
||||
|
||||
// Since one file is now in a folder, combine remaining visible items
|
||||
const remoteVisibleItems = [remoteFiles[1], remoteFiles[2], ...remoteFolders]
|
||||
|
||||
// Use ctrl/cmd-select for remote items
|
||||
await client.click(`.session-current .file-list.remote .sftp-item[title="${remoteVisibleItems[0]}"]`)
|
||||
await delay(500)
|
||||
|
||||
// Use keyboard modifier to select the rest of the items
|
||||
const isMac = process.platform === 'darwin'
|
||||
const modifier = isMac ? 'Meta' : 'Control'
|
||||
|
||||
for (let i = 1; i < remoteVisibleItems.length; i++) {
|
||||
const itemElement = await client.locator(`.session-current .file-list.remote .sftp-item[title="${remoteVisibleItems[i]}"]`)
|
||||
await itemElement.click({ modifiers: [modifier] })
|
||||
await delay(500)
|
||||
}
|
||||
|
||||
// Verify we selected the correct number of items
|
||||
const selectedRemoteItems = await client.locator('.session-current .file-list.remote .sftp-item.selected')
|
||||
const selectedRemoteCount = await selectedRemoteItems.count()
|
||||
expect(selectedRemoteCount).toBe(5) // 2 files + 3 folders
|
||||
|
||||
// Use context menu to download to local
|
||||
await client.rightClick('.session-current .file-list.remote .sftp-item.selected', 10, 10)
|
||||
await delay(1000)
|
||||
|
||||
// Click the download menu item
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item .anticon.anticon-cloud-download')
|
||||
await delay(10000) // Increased delay for folders with files to transfer
|
||||
|
||||
// Verify fileTransfers array is empty after the operation
|
||||
await verifyFileTransfersComplete(client)
|
||||
|
||||
// Verify all remote items were transferred to local, including folder structure
|
||||
for (const itemName of remoteVisibleItems) {
|
||||
expect(await verifyFileExists(client, 'local', itemName)).toBe(true)
|
||||
}
|
||||
|
||||
// Verify the file inside the transferred folder
|
||||
await enterFolder(client, 'local', remoteFolders[0])
|
||||
await delay(2000)
|
||||
expect(await verifyFileExists(client, 'local', remoteFiles[0])).toBe(true)
|
||||
await navigateToParentFolder(client, 'local')
|
||||
await delay(2000)
|
||||
|
||||
// Navigate back to parent folders
|
||||
await navigateToParentFolder(client, 'local')
|
||||
await delay(2000)
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
await delay(2000)
|
||||
|
||||
// Clean up - delete both test folders
|
||||
await deleteItem(client, 'local', mainFolder)
|
||||
await deleteItem(client, 'remote', mainFolder)
|
||||
await delay(2000)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteItem,
|
||||
enterFolder,
|
||||
navigateToParentFolder,
|
||||
verifyFileExists,
|
||||
verifyFileTransfersComplete,
|
||||
closeApp
|
||||
} = require('./common/common')
|
||||
|
||||
describe('drag-folder-with-files', function () {
|
||||
it('should drag folder with files in both directions', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
log('019.drag-folder.spec.js: app launched')
|
||||
|
||||
await setupSftpConnection(client)
|
||||
log('019.drag-folder.spec.js: sftp connected')
|
||||
|
||||
const stamp = Date.now()
|
||||
const localRoot = `drag-folder-local-root-${stamp}`
|
||||
const remoteRoot = `drag-folder-remote-root-${stamp}`
|
||||
const localSrcFolder = `local-src-folder-${stamp}`
|
||||
const localNestedFolder = `local-nested-folder-${stamp}`
|
||||
const localRootFile = `local-root-file-${stamp}.txt`
|
||||
const localNestedFile = `local-nested-file-${stamp}.txt`
|
||||
const remoteTargetForLocalDrag = `remote-target-folder-${stamp}`
|
||||
|
||||
const remoteSrcFolder = `remote-src-folder-${stamp}`
|
||||
const remoteNestedFolder = `remote-nested-folder-${stamp}`
|
||||
const remoteRootFile = `remote-root-file-${stamp}.txt`
|
||||
const remoteNestedFile = `remote-nested-file-${stamp}.txt`
|
||||
const localTargetForRemoteDrag = `local-target-folder-${stamp}`
|
||||
const localBasePath = await client.getValue('.session-current .sftp-local-section .sftp-title input')
|
||||
const localRootAbsPath = path.resolve(localBasePath, localRoot)
|
||||
|
||||
try {
|
||||
// Prepare isolated roots on both sides.
|
||||
await createFolder(client, 'local', localRoot)
|
||||
await createFolder(client, 'remote', remoteRoot)
|
||||
|
||||
// Build non-empty local source folder.
|
||||
await enterFolder(client, 'local', localRoot)
|
||||
await createFolder(client, 'local', localSrcFolder)
|
||||
await enterFolder(client, 'local', localSrcFolder)
|
||||
await createFile(client, 'local', localRootFile)
|
||||
await createFolder(client, 'local', localNestedFolder)
|
||||
await enterFolder(client, 'local', localNestedFolder)
|
||||
await createFile(client, 'local', localNestedFile)
|
||||
await navigateToParentFolder(client, 'local')
|
||||
await navigateToParentFolder(client, 'local')
|
||||
|
||||
// Build remote target folder for local -> remote drag.
|
||||
await enterFolder(client, 'remote', remoteRoot)
|
||||
await createFolder(client, 'remote', remoteTargetForLocalDrag)
|
||||
|
||||
// Drag local source folder into remote target folder.
|
||||
const localSourceSelector = `.session-current .file-list.local .sftp-item[title="${localSrcFolder}"]`
|
||||
const remoteTargetSelector = `.session-current .file-list.remote .sftp-item[title="${remoteTargetForLocalDrag}"]`
|
||||
await dragItemToItem(client, localSourceSelector, remoteTargetSelector)
|
||||
|
||||
await delay(8000)
|
||||
await verifyFileTransfersComplete(client)
|
||||
|
||||
// Verify local -> remote result and nested content.
|
||||
await enterFolder(client, 'remote', remoteTargetForLocalDrag)
|
||||
expect(await verifyFileExists(client, 'remote', localSrcFolder)).toBe(true)
|
||||
|
||||
await enterFolder(client, 'remote', localSrcFolder)
|
||||
expect(await verifyFileExists(client, 'remote', localRootFile)).toBe(true)
|
||||
expect(await verifyFileExists(client, 'remote', localNestedFolder)).toBe(true)
|
||||
|
||||
await enterFolder(client, 'remote', localNestedFolder)
|
||||
expect(await verifyFileExists(client, 'remote', localNestedFile)).toBe(true)
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
|
||||
// Build non-empty remote source folder for remote -> local drag.
|
||||
await createFolder(client, 'remote', remoteSrcFolder)
|
||||
await enterFolder(client, 'remote', remoteSrcFolder)
|
||||
await createFile(client, 'remote', remoteRootFile)
|
||||
await createFolder(client, 'remote', remoteNestedFolder)
|
||||
await enterFolder(client, 'remote', remoteNestedFolder)
|
||||
await createFile(client, 'remote', remoteNestedFile)
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
await navigateToParentFolder(client, 'remote')
|
||||
|
||||
// Build local target folder for remote -> local drag.
|
||||
await createFolder(client, 'local', localTargetForRemoteDrag)
|
||||
|
||||
// Drag remote source folder into local target folder.
|
||||
const remoteSourceSelector = `.session-current .file-list.remote .sftp-item[title="${remoteSrcFolder}"]`
|
||||
const localTargetSelector = `.session-current .file-list.local .sftp-item[title="${localTargetForRemoteDrag}"]`
|
||||
await dragItemToItem(client, remoteSourceSelector, localTargetSelector)
|
||||
|
||||
await delay(8000)
|
||||
await verifyFileTransfersComplete(client)
|
||||
|
||||
// Verify remote -> local result and nested content.
|
||||
await enterFolder(client, 'local', localTargetForRemoteDrag)
|
||||
expect(await verifyFileExists(client, 'local', remoteSrcFolder)).toBe(true)
|
||||
|
||||
await enterFolder(client, 'local', remoteSrcFolder)
|
||||
expect(await verifyFileExists(client, 'local', remoteRootFile)).toBe(true)
|
||||
expect(await verifyFileExists(client, 'local', remoteNestedFolder)).toBe(true)
|
||||
|
||||
await enterFolder(client, 'local', remoteNestedFolder)
|
||||
expect(await verifyFileExists(client, 'local', remoteNestedFile)).toBe(true)
|
||||
} finally {
|
||||
// Always clear local test folder even if assertions fail.
|
||||
await safeDeleteRoot(client, 'local', localRoot)
|
||||
await forceDeleteLocalPath(localRootAbsPath)
|
||||
// Keep remote cleanup as best effort to avoid leaking remote fixtures.
|
||||
await safeDeleteRoot(client, 'remote', remoteRoot)
|
||||
await closeApp(electronApp, __filename)
|
||||
log('019.drag-folder.spec.js: app closed')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
async function dragItemToItem (client, sourceSelector, targetSelector) {
|
||||
const sourceElement = await client.locator(sourceSelector)
|
||||
const targetElement = await client.locator(targetSelector)
|
||||
|
||||
const sourceBound = await sourceElement.boundingBox()
|
||||
const targetBound = await targetElement.boundingBox()
|
||||
|
||||
expect(!!sourceBound).toBe(true)
|
||||
expect(!!targetBound).toBe(true)
|
||||
|
||||
await client.mouse.move(
|
||||
sourceBound.x + sourceBound.width / 2,
|
||||
sourceBound.y + sourceBound.height / 2
|
||||
)
|
||||
await client.mouse.down()
|
||||
await delay(500)
|
||||
|
||||
await client.mouse.move(
|
||||
targetBound.x + targetBound.width / 2,
|
||||
targetBound.y + targetBound.height / 2,
|
||||
{ steps: 20 }
|
||||
)
|
||||
await delay(500)
|
||||
|
||||
await client.mouse.up()
|
||||
}
|
||||
|
||||
async function ensureItemVisibleAtCurrentOrParent (client, type, itemName, maxHops = 8) {
|
||||
const targetSelector = `.session-current .file-list.${type} .sftp-item[title="${itemName}"]`
|
||||
for (let i = 0; i <= maxHops; i++) {
|
||||
if (await client.elemExist(targetSelector)) {
|
||||
return true
|
||||
}
|
||||
await navigateToParentFolder(client, type)
|
||||
}
|
||||
throw new Error(`Failed to find ${itemName} in ${type} pane during cleanup`)
|
||||
}
|
||||
|
||||
async function safeDeleteRoot (client, type, itemName) {
|
||||
try {
|
||||
await ensureItemVisibleAtCurrentOrParent(client, type, itemName)
|
||||
await deleteItem(client, type, itemName)
|
||||
} catch (e) {
|
||||
log(`019.drag-folder.spec.js: cleanup skipped for ${type}/${itemName}: ${e.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function forceDeleteLocalPath (absPath) {
|
||||
try {
|
||||
if (!absPath || !fs.existsSync(absPath)) {
|
||||
return
|
||||
}
|
||||
fs.rmSync(absPath, { recursive: true, force: true })
|
||||
log(`019.drag-folder.spec.js: force deleted local path ${absPath}`)
|
||||
} catch (e) {
|
||||
log(`019.drag-folder.spec.js: force delete failed for ${absPath}: ${e.message}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(1000000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFile,
|
||||
createFolder,
|
||||
enterFolder,
|
||||
navigateToParentFolder,
|
||||
deleteItem,
|
||||
closeApp
|
||||
} = require('./common/common')
|
||||
|
||||
describe('file edit operations', function () {
|
||||
it('should properly handle file editing, search functionality and copy button', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
log('019.file-edit.spec.js: app launched')
|
||||
|
||||
// Set up SFTP connection
|
||||
await setupSftpConnection(client)
|
||||
log('019.file-edit.spec.js: sftp connection setup')
|
||||
|
||||
// Test local file editing (using context menu)
|
||||
await testFileEdit(client, 'local')
|
||||
log('019.file-edit.spec.js: local file edit tested')
|
||||
|
||||
// Test remote file editing (testing both context menu and double-click)
|
||||
await testFileEdit(client, 'remote', true)
|
||||
log('019.file-edit.spec.js: remote file edit tested')
|
||||
|
||||
log('019.file-edit.spec.js: calling close')
|
||||
await closeApp(electronApp, __filename)
|
||||
log('019.file-edit.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
|
||||
async function testFileEdit (client, type, testDoubleClick = false) {
|
||||
// Create a parent test folder
|
||||
const testFolderName = `edit-test-folder-${Date.now()}`
|
||||
await createFolder(client, type, testFolderName)
|
||||
await delay(2000)
|
||||
|
||||
// Enter the test folder
|
||||
await enterFolder(client, type, testFolderName)
|
||||
await delay(2000)
|
||||
|
||||
// Create a test file inside the folder
|
||||
const testFileName = `edit-test-file-${Date.now()}.txt`
|
||||
await createFile(client, type, testFileName)
|
||||
await delay(2000)
|
||||
|
||||
// Test 1: Open editor using context menu
|
||||
await client.rightClick(`.session-current .file-list.${type} .sftp-item[title="${testFileName}"]`, 10, 10)
|
||||
await delay(1000)
|
||||
|
||||
// Click the "Edit" option from context menu
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Edit")')
|
||||
await delay(3000) // Wait for editor modal to open
|
||||
|
||||
// Verify the editor modal is open
|
||||
await client.hasElem('.custom-modal-wrap .custom-modal-body textarea')
|
||||
|
||||
// Type multiline test content with repeating keywords for search testing
|
||||
const testContent = `This is a ${type} test content for the file editor!
|
||||
Second line of content with keyword.
|
||||
Third line has another example.
|
||||
Fourth line with keyword appearing again.
|
||||
Fifth line is the last example line.
|
||||
Keyword appears one final time here.`
|
||||
|
||||
await client.setValue('.custom-modal-wrap .custom-modal-body textarea', testContent)
|
||||
await delay(1000)
|
||||
|
||||
// Test 2: Test search functionality
|
||||
// Set a search term that appears multiple times - use the correct input selector
|
||||
const searchKeyword = 'keyword'
|
||||
await client.setValue('.custom-modal-wrap .custom-modal-body .ant-input-search input.ant-input', searchKeyword)
|
||||
await delay(500)
|
||||
|
||||
// Press Enter to trigger search
|
||||
// await client.keyboard.press('Enter')
|
||||
// await delay(1000)
|
||||
|
||||
// Verify that search found some matches (counter should show "1/3" or similar)
|
||||
const searchCounterText = await client.getText('.custom-modal-wrap .custom-modal-body .pd1x')
|
||||
expect(searchCounterText).not.toBe('0/0')
|
||||
|
||||
// Test next/previous navigation buttons
|
||||
// Click "next" button - use a more specific selector
|
||||
await client.click('.custom-modal-wrap .custom-modal-body button:has(.anticon-arrow-down)')
|
||||
await delay(500)
|
||||
|
||||
// Verify the counter incremented (should now be at "2/3" or similar)
|
||||
const nextSearchCounterText = await client.getText('.custom-modal-wrap .custom-modal-body .pd1x')
|
||||
expect(nextSearchCounterText).not.toBe(searchCounterText)
|
||||
|
||||
// Click "previous" button - use a more specific selector
|
||||
await client.click('.custom-modal-wrap .custom-modal-body button:has(.anticon-arrow-up)')
|
||||
await delay(500)
|
||||
|
||||
// Verify counter went back to original value
|
||||
const prevSearchCounterText = await client.getText('.custom-modal-wrap .custom-modal-body .pd1x')
|
||||
expect(prevSearchCounterText).toBe(searchCounterText)
|
||||
|
||||
// Test 3: Test the copy button
|
||||
// Clear the clipboard first
|
||||
await client.writeClipboard('')
|
||||
await delay(500)
|
||||
|
||||
// Click the copy button - use a more specific selector
|
||||
await client.click('.custom-modal-wrap .custom-modal-body button:has(.anticon-copy)')
|
||||
await delay(1000)
|
||||
|
||||
// Read clipboard content and verify it matches our test content
|
||||
const clipboardContent = await client.readClipboard()
|
||||
expect(clipboardContent).toBe(testContent)
|
||||
|
||||
// Click the save button
|
||||
await client.click('.custom-modal-wrap .custom-modal-body button:has-text("save")')
|
||||
await delay(1000) // Wait for save operation to complete
|
||||
|
||||
// For remote files, also test double-click to open editor
|
||||
if (type === 'remote' && testDoubleClick) {
|
||||
// Double-click on the file to open editor
|
||||
await client.doubleClick(`.session-current .file-list.${type} .sftp-item[title="${testFileName}"]`)
|
||||
await delay(3000) // Wait for editor modal to open
|
||||
|
||||
// Verify the editor modal is open
|
||||
await client.hasElem('.custom-modal-wrap .custom-modal-body textarea')
|
||||
|
||||
// Get the value from the editor to verify it contains our test content
|
||||
const editorValue = await client.getValue('.custom-modal-wrap .custom-modal-body textarea')
|
||||
expect(editorValue).toBe(testContent)
|
||||
|
||||
// Test search with a different keyword
|
||||
const altSearchKeyword = 'example'
|
||||
await client.setValue('.custom-modal-wrap .custom-modal-body .ant-input-search input.ant-input', altSearchKeyword)
|
||||
await delay(500)
|
||||
|
||||
// Press Enter to trigger search
|
||||
// await client.keyboard.press('Enter')
|
||||
// await delay(1000)
|
||||
|
||||
// Verify that search found matches
|
||||
const altSearchCounterText = await client.getText('.custom-modal-wrap .custom-modal-body .pd1x')
|
||||
expect(altSearchCounterText).not.toBe('0/0')
|
||||
|
||||
// Add more content and save (to verify double-click editing works)
|
||||
const additionalContent = '\nAdded through double-click access.'
|
||||
await client.setValue('.custom-modal-wrap .custom-modal-body textarea', testContent + additionalContent)
|
||||
await delay(1000)
|
||||
|
||||
// Save the updated content
|
||||
await client.click('.custom-modal-wrap .custom-modal-body button:has-text("save")')
|
||||
await delay(5000)
|
||||
}
|
||||
|
||||
// Final verification: Reopen the file
|
||||
await client.rightClick(`.session-current .file-list.${type} .sftp-item[title="${testFileName}"]`, 10, 10)
|
||||
await delay(1000)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Edit")')
|
||||
await delay(3000)
|
||||
|
||||
// Get the value from the editor to verify the content
|
||||
const editorValue = await client.getValue('.custom-modal-wrap .custom-modal-body textarea')
|
||||
if (type === 'remote' && testDoubleClick) {
|
||||
expect(editorValue).toBe(testContent + '\nAdded through double-click access.')
|
||||
} else {
|
||||
expect(editorValue).toBe(testContent)
|
||||
}
|
||||
|
||||
// Close the editor using cancel button
|
||||
await client.click('.custom-modal-wrap .custom-modal-body button:has-text("cancel")')
|
||||
await delay(2000)
|
||||
|
||||
// Navigate back to the parent folder
|
||||
await navigateToParentFolder(client, type)
|
||||
await delay(2000)
|
||||
|
||||
// Clean up - delete the test folder
|
||||
await deleteItem(client, type, testFolderName)
|
||||
await delay(2000)
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteItem,
|
||||
enterFolder,
|
||||
navigateToParentFolder,
|
||||
verifyFileExists,
|
||||
verifyFileTransfersComplete,
|
||||
closeApp
|
||||
} = require('./common/common')
|
||||
|
||||
describe('multi-file-drag-drop-operation', function () {
|
||||
it('should test multiple file selection and drag-drop operations', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
log('019.file-op-drag-to-folder.spec.js: app launched')
|
||||
|
||||
await setupSftpConnection(client)
|
||||
log('019.file-op-drag-to-folder.spec.js: sftp connected')
|
||||
|
||||
// Test for both local and remote
|
||||
await testMultiFileDragDrop(client, 'local')
|
||||
log('019.file-op-drag-to-folder.spec.js: local drag drop tested')
|
||||
await testMultiFileDragDrop(client, 'remote')
|
||||
log('019.file-op-drag-to-folder.spec.js: remote drag drop tested')
|
||||
|
||||
log('019.file-op-drag-to-folder.spec.js: calling close')
|
||||
await closeApp(electronApp, __filename)
|
||||
log('019.file-op-drag-to-folder.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Select files by clicking the first and shift-clicking the last,
|
||||
* then drag them into targetFolderName.
|
||||
*/
|
||||
async function dragFilesToFolder (client, type, fileNames, targetFolderName) {
|
||||
// Select first file
|
||||
await client.click(`.session-current .file-list.${type} .sftp-item[title="${fileNames[0]}"]`)
|
||||
await delay(500)
|
||||
|
||||
// Shift-click last file to select all
|
||||
const lastFile = client.locator(`.session-current .file-list.${type} .sftp-item[title="${fileNames[fileNames.length - 1]}"]`)
|
||||
await lastFile.click({ modifiers: ['Shift'] })
|
||||
await delay(1000)
|
||||
|
||||
const sourceBound = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${fileNames[0]}"]`).boundingBox()
|
||||
const targetBound = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${targetFolderName}"]`).boundingBox()
|
||||
|
||||
await client.mouse.move(
|
||||
sourceBound.x + sourceBound.width / 2,
|
||||
sourceBound.y + sourceBound.height / 2
|
||||
)
|
||||
await client.mouse.down()
|
||||
await delay(500)
|
||||
await client.mouse.move(
|
||||
targetBound.x + targetBound.width / 2,
|
||||
targetBound.y + targetBound.height / 2,
|
||||
{ steps: 20 }
|
||||
)
|
||||
await delay(500)
|
||||
await client.mouse.up()
|
||||
await delay(3000)
|
||||
}
|
||||
|
||||
async function testMultiFileDragDrop (client, type) {
|
||||
// Create a main test folder first
|
||||
const mainFolderName = `main-test-folder-${Date.now()}`
|
||||
await createFolder(client, type, mainFolderName)
|
||||
|
||||
// Enter the main test folder
|
||||
await enterFolder(client, type, mainFolderName)
|
||||
await delay(2000)
|
||||
|
||||
// Create target folder for drag-drop operation
|
||||
const targetFolderName = `target-folder-${Date.now()}`
|
||||
await createFolder(client, type, targetFolderName)
|
||||
|
||||
// Create 3 test files
|
||||
const fileNames = [
|
||||
`file-1-${Date.now()}.txt`,
|
||||
`file-2-${Date.now()}.txt`,
|
||||
`file-3-${Date.now()}.txt`
|
||||
]
|
||||
|
||||
for (const fileName of fileNames) {
|
||||
await createFile(client, type, fileName)
|
||||
await delay(800)
|
||||
}
|
||||
|
||||
// --- Drag 1: files → targetFolder (no conflict) ---
|
||||
await dragFilesToFolder(client, type, fileNames, targetFolderName)
|
||||
|
||||
// Verify transfers complete
|
||||
await verifyFileTransfersComplete(client)
|
||||
|
||||
// Enter target folder to verify files were moved
|
||||
await enterFolder(client, type, targetFolderName)
|
||||
await delay(2000)
|
||||
|
||||
// Verify all three files exist in the target folder
|
||||
for (const fileName of fileNames) {
|
||||
expect(await verifyFileExists(client, type, fileName)).toBe(true)
|
||||
}
|
||||
|
||||
// --- Drag files back to parent folder using parent-file-item ---
|
||||
// Select all three files by clicking first and shift-clicking last
|
||||
await client.click(`.session-current .file-list.${type} .sftp-item[title="${fileNames[0]}"]`)
|
||||
await delay(500)
|
||||
|
||||
const lastFileElement = client.locator(`.session-current .file-list.${type} .sftp-item[title="${fileNames[2]}"]`)
|
||||
await lastFileElement.click({ modifiers: ['Shift'] })
|
||||
await delay(1000)
|
||||
|
||||
// Verify all three files are selected
|
||||
for (const fileName of fileNames) {
|
||||
const item = client.locator(`.session-current .file-list.${type} .sftp-item[title="${fileName}"]`)
|
||||
const isSelected = await item.evaluate(el => el.classList.contains('selected'))
|
||||
expect(isSelected).toBe(true)
|
||||
}
|
||||
|
||||
// Drag from first file to parent-file-item
|
||||
const dragSourceBound = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${fileNames[0]}"]`).boundingBox()
|
||||
const parentFileBound = await client.locator(`.session-current .file-list.${type} .parent-file-item`).boundingBox()
|
||||
|
||||
await client.mouse.move(
|
||||
dragSourceBound.x + dragSourceBound.width / 2,
|
||||
dragSourceBound.y + dragSourceBound.height / 2
|
||||
)
|
||||
await client.mouse.down()
|
||||
await delay(500)
|
||||
await client.mouse.move(
|
||||
parentFileBound.x + parentFileBound.width / 2,
|
||||
parentFileBound.y + parentFileBound.height / 2,
|
||||
{ steps: 20 }
|
||||
)
|
||||
await delay(500)
|
||||
await client.mouse.up()
|
||||
await delay(3000)
|
||||
|
||||
// Verify transfers complete
|
||||
await verifyFileTransfersComplete(client)
|
||||
|
||||
// Go back to mainFolder and verify files are there
|
||||
await navigateToParentFolder(client, type)
|
||||
await delay(2000)
|
||||
|
||||
for (const fileName of fileNames) {
|
||||
expect(await verifyFileExists(client, type, fileName)).toBe(true)
|
||||
}
|
||||
|
||||
// Verify target folder is empty
|
||||
await enterFolder(client, type, targetFolderName)
|
||||
await delay(2000)
|
||||
|
||||
// The target folder should only have the parent-file-item and hidden-file-item
|
||||
const itemCount = await client.locator(`.session-current .file-list.${type} .sftp-item`).count()
|
||||
expect(itemCount).toBe(2) // Only parent-file-item and hidden-file-item
|
||||
|
||||
// Go back to mainFolder
|
||||
await navigateToParentFolder(client, type)
|
||||
await delay(2000)
|
||||
|
||||
// --- Conflict test setup: create same-named files in targetFolder ---
|
||||
// Files are now in mainFolder, targetFolder is empty.
|
||||
// Populate targetFolder with same file names so subsequent drags produce conflicts.
|
||||
await enterFolder(client, type, targetFolderName)
|
||||
await delay(1500)
|
||||
|
||||
for (const fileName of fileNames) {
|
||||
await createFile(client, type, fileName)
|
||||
await delay(500)
|
||||
}
|
||||
|
||||
// Navigate back to mainFolder
|
||||
await navigateToParentFolder(client, type)
|
||||
await delay(1500)
|
||||
|
||||
// --- Drag 2 (conflict): files → targetFolder, resolve with Rename ---
|
||||
// mainFolder has 3 files; targetFolder already has the same file names → conflict.
|
||||
await dragFilesToFolder(client, type, fileNames, targetFolderName)
|
||||
|
||||
// Handle conflict with rename (reused from 018 pattern)
|
||||
await client.click('.custom-modal-footer button:has-text("Rename all")')
|
||||
await delay(3000)
|
||||
await verifyFileTransfersComplete(client)
|
||||
|
||||
// Enter targetFolder and verify renamed items exist
|
||||
await enterFolder(client, type, targetFolderName)
|
||||
await delay(1500)
|
||||
|
||||
const renamedItems = client.locator(`.session-current .file-list.${type} .sftp-item[title*="(rename-"]`)
|
||||
const renamedCount = await renamedItems.count()
|
||||
expect(renamedCount).toBeGreaterThan(0)
|
||||
log(`019: Found ${renamedCount} renamed items in ${type} ${targetFolderName}`)
|
||||
|
||||
// Navigate back to mainFolder
|
||||
await navigateToParentFolder(client, type)
|
||||
await delay(1500)
|
||||
|
||||
// --- Drag 3 (conflict): files → targetFolder, resolve with Overwrite ---
|
||||
// After the rename drag, mainFolder files were moved; recreate them.
|
||||
for (const fileName of fileNames) {
|
||||
await createFile(client, type, fileName)
|
||||
await delay(500)
|
||||
}
|
||||
|
||||
// targetFolder still has the original-named files → conflict on overwrite.
|
||||
await dragFilesToFolder(client, type, fileNames, targetFolderName)
|
||||
|
||||
// Handle conflict with overwrite (reused from 018 pattern)
|
||||
await client.click('.custom-modal-footer button:has-text("Overwrite all")')
|
||||
await delay(3000)
|
||||
await verifyFileTransfersComplete(client)
|
||||
|
||||
// Enter targetFolder and verify original file names still exist (overwritten)
|
||||
await enterFolder(client, type, targetFolderName)
|
||||
await delay(1500)
|
||||
|
||||
for (const fileName of fileNames) {
|
||||
expect(await verifyFileExists(client, type, fileName)).toBe(true)
|
||||
}
|
||||
log(`019: Overwrite conflict resolution verified in ${type} ${targetFolderName}`)
|
||||
|
||||
// Navigate back to mainFolder
|
||||
await navigateToParentFolder(client, type)
|
||||
await delay(1500)
|
||||
|
||||
// Go back to parent folder
|
||||
await navigateToParentFolder(client, type)
|
||||
await delay(2000)
|
||||
|
||||
// Clean up - delete the main test folder (which contains everything)
|
||||
await deleteItem(client, type, mainFolderName)
|
||||
await delay(2000)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(1000000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
|
||||
const {
|
||||
setupSftpConnection,
|
||||
createFolder,
|
||||
createFile,
|
||||
deleteItem,
|
||||
enterFolder,
|
||||
navigateToParentFolder,
|
||||
selectItemsWithCtrlOrCmd,
|
||||
closeApp
|
||||
} = require('./common/common')
|
||||
|
||||
describe('multi-file selection operations', function () {
|
||||
it('should properly handle single and multi-file selection with modifier keys', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
log('019.file-select.spec.js: app launched')
|
||||
|
||||
// Set up SFTP connection
|
||||
await setupSftpConnection(client)
|
||||
log('019.file-select.spec.js: sftp connected')
|
||||
|
||||
// Test both local and remote file systems
|
||||
await testMultiFileSelection(client, 'local')
|
||||
log('019.file-select.spec.js: local selection tested')
|
||||
await testMultiFileSelection(client, 'remote')
|
||||
log('019.file-select.spec.js: remote selection tested')
|
||||
|
||||
await closeApp(electronApp, __filename)
|
||||
log('019.file-select.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
|
||||
async function testMultiFileSelection (client, type) {
|
||||
// Create a parent test folder
|
||||
const testFolderName = `selection-test-folder-${Date.now()}`
|
||||
await createFolder(client, type, testFolderName)
|
||||
|
||||
// Enter the test folder
|
||||
await enterFolder(client, type, testFolderName)
|
||||
await delay(2000)
|
||||
|
||||
// Create multiple files and folders for testing selection
|
||||
const itemNames = [
|
||||
`file-1-${Date.now()}.txt`,
|
||||
`file-2-${Date.now()}.txt`,
|
||||
`file-3-${Date.now()}.txt`,
|
||||
`folder-1-${Date.now()}`,
|
||||
`folder-2-${Date.now()}`
|
||||
]
|
||||
|
||||
// Create the test items
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await createFile(client, type, itemNames[i])
|
||||
}
|
||||
|
||||
for (let i = 3; i < 5; i++) {
|
||||
await createFolder(client, type, itemNames[i])
|
||||
}
|
||||
|
||||
// Test 1: Single click selection
|
||||
await client.click(`.session-current .file-list.${type} .sftp-item[title="${itemNames[0]}"]`)
|
||||
await delay(500)
|
||||
|
||||
// Verify only one item is selected
|
||||
const fileItems = await client.locator(`.session-current .file-list.${type} .real-file-item`)
|
||||
|
||||
let selectedCount = await countSelectedItems(client, fileItems)
|
||||
expect(selectedCount).toBe(1)
|
||||
|
||||
// Verify the correct item is selected
|
||||
const firstItemSelected = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${itemNames[0]}"]`)
|
||||
const hasSelectedClass = await firstItemSelected.evaluate(el => el.classList.contains('selected'))
|
||||
expect(hasSelectedClass).toBe(true)
|
||||
|
||||
// Test 2: Click another item should deselect the first
|
||||
await client.click(`.session-current .file-list.${type} .sftp-item[title="${itemNames[1]}"]`)
|
||||
await delay(500)
|
||||
|
||||
// Verify only the second item is selected now
|
||||
selectedCount = await countSelectedItems(client, fileItems)
|
||||
expect(selectedCount).toBe(1)
|
||||
|
||||
const secondItemSelected = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${itemNames[1]}"]`)
|
||||
const secondHasSelectedClass = await secondItemSelected.evaluate(el => el.classList.contains('selected'))
|
||||
expect(secondHasSelectedClass).toBe(true)
|
||||
|
||||
// Verify first item is no longer selected
|
||||
const firstItemStillSelected = await firstItemSelected.evaluate(el => el.classList.contains('selected'))
|
||||
expect(firstItemStillSelected).toBe(false)
|
||||
|
||||
// Test 3: Ctrl/Cmd+click for multi-selection
|
||||
// Select items at index 0, 2, and 4 (first file, third file, and second folder)
|
||||
await selectItemsWithCtrlOrCmd(client, type, [0, 2, 4])
|
||||
await delay(1000)
|
||||
|
||||
// Verify exactly three items are selected
|
||||
selectedCount = await countSelectedItems(client, fileItems)
|
||||
expect(selectedCount).toBe(3)
|
||||
|
||||
// Verify the specific items are selected
|
||||
const itemsToCheck = [itemNames[0], itemNames[2], itemNames[4]]
|
||||
for (const itemName of itemsToCheck) {
|
||||
const item = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${itemName}"]`)
|
||||
const isSelected = await item.evaluate(el => el.classList.contains('selected'))
|
||||
expect(isSelected).toBe(true)
|
||||
}
|
||||
|
||||
// Test 4: Single click again should deselect all and select only one
|
||||
await client.click(`.session-current .file-list.${type} .sftp-item[title="${itemNames[3]}"]`)
|
||||
await delay(500)
|
||||
|
||||
// Verify only one item is selected
|
||||
selectedCount = await countSelectedItems(client, fileItems)
|
||||
expect(selectedCount).toBe(1)
|
||||
|
||||
// Verify the fourth item (folder-1) is selected
|
||||
const fourthItemSelected = await client.locator(`.session-current .file-list.${type} .sftp-item[title="${itemNames[3]}"]`)
|
||||
const fourthHasSelectedClass = await fourthItemSelected.evaluate(el => el.classList.contains('selected'))
|
||||
expect(fourthHasSelectedClass).toBe(true)
|
||||
|
||||
// Navigate back to parent folder
|
||||
await navigateToParentFolder(client, type)
|
||||
await delay(1500)
|
||||
|
||||
// Clean up by deleting the test folder
|
||||
await deleteItem(client, type, testFolderName)
|
||||
await delay(2000)
|
||||
}
|
||||
|
||||
async function countSelectedItems (client, fileItems) {
|
||||
const count = await fileItems.count()
|
||||
let selectedCount = 0
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const hasSelectedClass = await fileItems.nth(i).evaluate(el => el.classList.contains('selected'))
|
||||
if (hasSelectedClass) {
|
||||
selectedCount++
|
||||
}
|
||||
}
|
||||
|
||||
return selectedCount
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
require('dotenv').config()
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const { expect } = require('./common/expect')
|
||||
const appOptions = require('./common/app-options')
|
||||
const e = require('./common/lang')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { closeApp } = require('./common/common')
|
||||
const {
|
||||
GIST_ID,
|
||||
GIST_TOKEN,
|
||||
GITEE_TOKEN,
|
||||
GITEE_ID,
|
||||
CUSTOM_SYNC_URL,
|
||||
CUSTOM_SYNC_USER,
|
||||
CUSTOM_SYNC_SECRET,
|
||||
CLOUD_TOKEN
|
||||
} = process.env
|
||||
|
||||
describe('data sync', function () {
|
||||
it('should open sync page', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
|
||||
await delay(3500)
|
||||
|
||||
async function checkNoError () {
|
||||
const hasError = await client.elemExist('.common-err-desc')
|
||||
expect(hasError).equal(false)
|
||||
}
|
||||
|
||||
log('button:open sync')
|
||||
await client.click('.btns .anticon-cloud-sync')
|
||||
await delay(500)
|
||||
const sel = '.setting-wrap .ant-tabs-nav-list .ant-tabs-tab-active'
|
||||
await client.hasElem(sel)
|
||||
await delay(500)
|
||||
const text = await client.getText(sel)
|
||||
expect(text.toLowerCase()).equal(e('setting').toLowerCase())
|
||||
|
||||
// create theme
|
||||
log('save github token / id')
|
||||
|
||||
await client.setValue('#sync-input-token-github', GIST_TOKEN)
|
||||
await client.setValue('#sync-input-gistId-github', GIST_ID)
|
||||
await delay(1000)
|
||||
await client.click('.setting-wrap .sync-btn-save')
|
||||
await delay(5000)
|
||||
await client.click('.setting-wrap .sync-btn-down')
|
||||
await delay(5000)
|
||||
const bks = await client.evaluate(() => {
|
||||
return window.store.bookmarks
|
||||
})
|
||||
expect(bks.length > 3).equal(true)
|
||||
await checkNoError()
|
||||
|
||||
log('save gitee token / id')
|
||||
|
||||
await client.click('.setting-wrap [id*="tab-gitee"]')
|
||||
await client.evaluate(() => {
|
||||
return window.store.setBookmarks([])
|
||||
})
|
||||
await delay(3000)
|
||||
|
||||
await client.setValue('#sync-input-token-gitee', GITEE_TOKEN)
|
||||
await client.setValue('#sync-input-gistId-gitee', GITEE_ID)
|
||||
await delay(1000)
|
||||
await client.click('.setting-wrap .sync-btn-save:visible')
|
||||
await delay(6000)
|
||||
await client.click('.setting-wrap .sync-btn-down:visible')
|
||||
await delay(6000)
|
||||
const bks1 = await client.evaluate(() => {
|
||||
return window.store.bookmarks
|
||||
})
|
||||
expect(bks1.length > 3).equal(true)
|
||||
await checkNoError()
|
||||
|
||||
log('save custom props')
|
||||
|
||||
await client.click('.setting-wrap [id*="tab-custom"]')
|
||||
await client.evaluate(() => {
|
||||
return window.store.setBookmarks([])
|
||||
})
|
||||
await delay(3000)
|
||||
|
||||
await client.setValue('#sync-input-url-custom', CUSTOM_SYNC_URL)
|
||||
await client.setValue('#sync-input-token-custom', CUSTOM_SYNC_SECRET)
|
||||
await client.setValue('#sync-input-gistId-custom', CUSTOM_SYNC_USER)
|
||||
await delay(1000)
|
||||
await client.click('.setting-wrap .sync-btn-save:visible')
|
||||
await delay(6000)
|
||||
await client.click('.setting-wrap .sync-btn-down:visible')
|
||||
await delay(6000)
|
||||
const bks3 = await client.evaluate(() => {
|
||||
return window.store.bookmarks
|
||||
})
|
||||
expect(bks3.length > 3).equal(true)
|
||||
await checkNoError()
|
||||
|
||||
log('018.sync.spec.js: save cloud props')
|
||||
|
||||
await client.click('.setting-wrap [id*="tab-cloud"]')
|
||||
log('018.sync.spec.js: cloud tab clicked')
|
||||
await delay(3000)
|
||||
|
||||
await client.setValue('#sync-input-token-cloud', CLOUD_TOKEN)
|
||||
log('018.sync.spec.js: cloud token set')
|
||||
await delay(1000)
|
||||
await client.click('.setting-wrap .sync-btn-save:visible')
|
||||
log('018.sync.spec.js: cloud save clicked')
|
||||
await delay(6000)
|
||||
await client.click('.setting-wrap .sync-btn-up:visible')
|
||||
log('018.sync.spec.js: cloud up clicked')
|
||||
await delay(6000)
|
||||
await client.evaluate(() => {
|
||||
return window.store.setBookmarks([])
|
||||
})
|
||||
log('018.sync.spec.js: bookmarks cleared')
|
||||
await client.click('.setting-wrap .sync-btn-down:visible')
|
||||
log('018.sync.spec.js: cloud down clicked')
|
||||
await delay(6000)
|
||||
const bks4 = await client.evaluate(() => {
|
||||
return window.store.bookmarks
|
||||
})
|
||||
expect(bks4.length > 3).equal(true)
|
||||
log('018.sync.spec.js: cloud sync verified')
|
||||
|
||||
await delay(1000)
|
||||
log('018.sync.spec.js: calling close')
|
||||
await closeApp(electronApp, __filename)
|
||||
log('018.sync.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* quick commands test
|
||||
* need TEST_HOST TEST_PASS TEST_USER env set
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { closeApp } = require('./common/common')
|
||||
|
||||
describe('profile', function () {
|
||||
it('quick commands form', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
log('0103.profile.spec.js: open setting')
|
||||
await delay(2000)
|
||||
await client.click('.btns .anticon-setting')
|
||||
await delay(2500)
|
||||
log('0103.profile.spec.js: setting opened')
|
||||
log('0103.profile.spec.js: click profiles')
|
||||
await client.click('.setting-tabs [role="tab"]', 4)
|
||||
log('0103.profile.spec.js: profiles tab clicked')
|
||||
// await client.click('.setting-tabs [role="tab"]', 4)
|
||||
await client.setValue(
|
||||
'.setting-tabs-profile input#name',
|
||||
'profile1'
|
||||
)
|
||||
log('0103.profile.spec.js: name set')
|
||||
await client.setValue(
|
||||
'.setting-tabs-profile input#password',
|
||||
'zxd'
|
||||
)
|
||||
log('0103.profile.spec.js: password set')
|
||||
const qmlist1 = await client.countElem('.setting-tabs-profile .item-list-unit')
|
||||
await delay(150)
|
||||
await client.click('.setting-tabs-profile .ant-btn-primary')
|
||||
log('0103.profile.spec.js: save button clicked')
|
||||
await delay(2550)
|
||||
const qmlist2 = await client.countElem('.setting-tabs-profile .item-list-unit')
|
||||
expect(qmlist2).equal(qmlist1 + 1)
|
||||
log('0103.profile.spec.js: profile added')
|
||||
|
||||
await delay(1150)
|
||||
const c1 = await client.evaluate(() => {
|
||||
return window.store.profiles.length
|
||||
})
|
||||
expect(qmlist1).equal(c1)
|
||||
log('0103.profile.spec.js: profile count verified')
|
||||
await closeApp(electronApp, __filename)
|
||||
log('0103.profile.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* ssh profile login test
|
||||
*/
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const {
|
||||
TEST_PASS,
|
||||
TEST_USER
|
||||
} = require('./common/env')
|
||||
const uid = require('./common/uid')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const {
|
||||
closeApp,
|
||||
setupSshConnection
|
||||
} = require('./common/common')
|
||||
const { basicTerminalTest } = require('./common/basic-terminal-test')
|
||||
|
||||
describe('ssh profile login', function () {
|
||||
it('should create profile and login using it', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
// First create an SSH connection to get proper tab data
|
||||
await setupSshConnection(client, {
|
||||
waitAfterConnect: 5500
|
||||
})
|
||||
log('0104.profile-use.spec.js: ssh form submitted')
|
||||
log('0104.profile-use.spec.js: ssh connected')
|
||||
|
||||
// Get the SSH tab data
|
||||
const sshTab = await client.evaluate(() => {
|
||||
return window.store.tabs[window.store.tabs.length - 1]
|
||||
})
|
||||
log('0104.profile-use.spec.js: ssh tab data retrieved')
|
||||
|
||||
// Create profile
|
||||
await client.click('.btns .anticon-setting')
|
||||
log('0104.profile-use.spec.js: setting opened')
|
||||
await delay(2500)
|
||||
await client.click('.setting-tabs [role="tab"]', 4)
|
||||
log('0104.profile-use.spec.js: profiles tab clicked')
|
||||
await delay(500)
|
||||
|
||||
const profileName = 'Test-Profile-' + uid()
|
||||
await client.setValue('.setting-tabs-profile input#name', profileName)
|
||||
log('0104.profile-use.spec.js: profile name set')
|
||||
await client.setValue('.setting-tabs-profile input#password', TEST_PASS)
|
||||
await client.setValue('.setting-tabs-profile input#username', TEST_USER)
|
||||
await delay(150)
|
||||
|
||||
const profileCountPrev = await client.evaluate(() => {
|
||||
return window.store.profiles.length
|
||||
})
|
||||
await client.click('.setting-tabs-profile .ant-btn-primary')
|
||||
log('0104.profile-use.spec.js: profile save clicked')
|
||||
await delay(2550)
|
||||
|
||||
// Verify profile was created
|
||||
const profileCount = await client.evaluate(() => {
|
||||
return window.store.profiles.length
|
||||
})
|
||||
expect(profileCount).equal(profileCountPrev + 1)
|
||||
log('0104.profile-use.spec.js: profile created')
|
||||
|
||||
// Get profile ID and create tab with it
|
||||
await client.evaluate((data) => {
|
||||
window.store.showModal = 0
|
||||
})
|
||||
await client.evaluate((data) => {
|
||||
const profile = window.store.profiles.find(p => p.name === data.name)
|
||||
const nt = {
|
||||
...data.tab,
|
||||
id: 'sdfdsf',
|
||||
pid: '',
|
||||
profile: profile.id
|
||||
}
|
||||
nt.authType = 'profiles'
|
||||
delete nt.password
|
||||
window.store.addTab(nt)
|
||||
}, {
|
||||
name: profileName,
|
||||
tab: sshTab
|
||||
})
|
||||
log('0104.profile-use.spec.js: tab with profile added')
|
||||
await delay(5500)
|
||||
|
||||
// Verify connection works
|
||||
await basicTerminalTest(client, 'ls')
|
||||
log('0104.profile-use.spec.js: basic terminal test done')
|
||||
|
||||
await closeApp(electronApp, __filename)
|
||||
log('0104.profile-use.spec.js: app closed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const { expect } = require('./common/expect')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('symbolic links support', function () {
|
||||
it('symbolic links support works', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
const tmp = 'tmp-' + (+new Date())
|
||||
const cmd = `cd ~ && mkdir ${tmp} && cd ${tmp} && touch x.js && mkdir xx && ln -s x.js xk && ln -s xx xxk`
|
||||
await client.keyboard.type(cmd)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(1900)
|
||||
await client.click('.session-current .term-sftp-tabs .fileManager')
|
||||
await delay(300)
|
||||
await client.click('.session-current .anticon-reload')
|
||||
await delay(2500)
|
||||
await client.doubleClick('.session-current .file-list.local .real-file-item')
|
||||
|
||||
await delay(5000)
|
||||
const localFileList = await client.countElem('.session-current .file-list.local .sftp-item')
|
||||
expect(localFileList >= 5).equal(true)
|
||||
await client.click('.session-current .term-sftp-tabs .terminal')
|
||||
await delay(500)
|
||||
const cmd1 = `cd ~ && rm -rf ${tmp}`
|
||||
await client.keyboard.type(cmd1)
|
||||
await client.keyboard.press('Enter')
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it,
|
||||
expect
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const e = require('./common/lang')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('init setting buttons', function () {
|
||||
it('all buttons open proper setting tab', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
// await client.waitUntilWindowLoaded()
|
||||
await delay(3500)
|
||||
log('close current tab')
|
||||
await client.hover('.tabs .tab')
|
||||
await client.click('.tabs .tab .tab-close')
|
||||
await delay(1000)
|
||||
|
||||
log('verify tab count')
|
||||
const tabCount = await client.countElem('.tabs .tab')
|
||||
expect(tabCount).toEqual(0)
|
||||
|
||||
log('button:edit')
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(3500)
|
||||
const sel = '.setting-wrap .ant-tabs-nav-list .ant-tabs-tab-active'
|
||||
const active = await client.element(sel)
|
||||
expect(active).toBeVisible()
|
||||
const text = await client.getText(sel)
|
||||
expect(text).toEqual(e('bookmarks'))
|
||||
|
||||
log('close')
|
||||
await client.click('.setting-wrap .close-setting-wrap')
|
||||
await delay(900)
|
||||
|
||||
log('open setting')
|
||||
await client.click('.btns .anticon-setting')
|
||||
await delay(2500)
|
||||
const active1 = await client.element(sel)
|
||||
expect(active1).toBeVisible()
|
||||
const text1 = await client.getText(sel)
|
||||
expect(text1).toEqual(e('setting'))
|
||||
log('close')
|
||||
await client.click('.setting-wrap .close-setting-wrap')
|
||||
await delay(900)
|
||||
|
||||
log('button:new ssh')
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(1000)
|
||||
const active2 = await client.element(sel)
|
||||
expect(active2).toBeVisible()
|
||||
const text2 = await client.getText(sel)
|
||||
expect(text2).toEqual(e('bookmarks'))
|
||||
|
||||
// log('tab it')
|
||||
// await client.click('.setting-wrap .ant-tabs-tab:nth-child(3)')
|
||||
// await delay(3100)
|
||||
// const text4 = await client.getText(sel)
|
||||
// expect(text4).toEqual(e('setting'))
|
||||
await client.click('.setting-wrap .close-setting-wrap')
|
||||
await delay(600)
|
||||
|
||||
log('button:edit again')
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(600)
|
||||
const text5 = await client.getText(sel)
|
||||
expect(text5).toEqual(e('bookmarks'))
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const { expect } = require('./common/expect')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const log = require('./common/log')
|
||||
|
||||
describe('tabs', function () {
|
||||
it('double click to duplicate tab button works', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
const tabsLenBefore = await client.countElem('.tabs .tab')
|
||||
const wrapsBefore = await client.countElem('.sessions > div')
|
||||
log('double click tab')
|
||||
await client.doubleClick('.tab')
|
||||
await delay(1500)
|
||||
const tabs0 = await client.countElem('.tabs .tab')
|
||||
expect(tabs0).equal(tabsLenBefore + 1)
|
||||
const wraps = await client.countElem('.sessions > div')
|
||||
expect(wraps).equal(wrapsBefore + 1)
|
||||
await delay(500)
|
||||
log('click add tab icon')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(500)
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
const tabs1 = await client.countElem('.tabs .tab')
|
||||
expect(tabs1).equal(tabsLenBefore + 2)
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Test tab reload context menu
|
||||
* Need TEST_HOST, TEST_PASS, TEST_USER env set
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('tab reload', function () {
|
||||
it('should replace active tab and update activeTabId', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
|
||||
// Create 2 tabs
|
||||
await client.click('.tabs-add-btn')
|
||||
await delay(500)
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(2000)
|
||||
|
||||
// Get initial state
|
||||
const initialTabs = await client.evaluate(() => ({
|
||||
tabs: window.store.tabs,
|
||||
activeId: window.store.activeTabId
|
||||
}))
|
||||
|
||||
// Reload active tab
|
||||
await client.rightClick(`.tabs .tab[data-id="${initialTabs.activeId}"]`, 30, 20)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Reload")')
|
||||
await delay(2000)
|
||||
|
||||
// Verify replacement
|
||||
const afterReload = await client.evaluate(() => ({
|
||||
tabs: window.store.tabs,
|
||||
activeId: window.store.activeTabId
|
||||
}))
|
||||
|
||||
expect(afterReload.tabs.length).equal(initialTabs.tabs.length)
|
||||
expect(initialTabs.tabs.some(t => t.id === afterReload.activeId)).equal(false)
|
||||
expect(afterReload.activeId !== initialTabs.activeId).equal(true)
|
||||
expect(afterReload.activeId === afterReload.tabs[1].id).equal(true)
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
|
||||
it('should keep activeTabId when reloading inactive tab', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
|
||||
// Create 2 tabs
|
||||
await client.click('.tabs-add-btn')
|
||||
await delay(500)
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(2000)
|
||||
|
||||
// Switch to first tab and get inactive tab ID
|
||||
await client.click('.tabs .tab:first-child')
|
||||
await delay(1000)
|
||||
const { tabs, activeId } = await client.evaluate(() => ({
|
||||
tabs: window.store.tabs,
|
||||
activeId: window.store.activeTabId
|
||||
}))
|
||||
const inactiveTab = tabs.find(t => t.id !== activeId)
|
||||
|
||||
// Reload inactive tab
|
||||
await client.rightClick(`.tabs .tab[data-id="${inactiveTab.id}"]`, 30, 20)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Reload")')
|
||||
await delay(2000)
|
||||
|
||||
// Verify state
|
||||
const afterReload = await client.evaluate(() => ({
|
||||
tabs: window.store.tabs,
|
||||
activeId: window.store.activeTabId
|
||||
}))
|
||||
|
||||
expect(afterReload.tabs.length).equal(tabs.length)
|
||||
expect(afterReload.activeId).equal(activeId)
|
||||
expect(afterReload.tabs.some(t => t.id === inactiveTab.id)).equal(false)
|
||||
|
||||
await electronApp.close()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* basic ssh test
|
||||
* need TEST_HOST TEST_PASS TEST_USER env set
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const { expect } = require('./common/expect')
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('ssh', function () {
|
||||
it('should open window and basic ssh ls command works', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(4500)
|
||||
await client.rightClick('.tabs .tab', 30, 20)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .anticon-copy')
|
||||
await delay(2500)
|
||||
const tabsCount = await client.evaluate(() => {
|
||||
return window.store.tabs.length
|
||||
})
|
||||
expect(tabsCount).equal(2)
|
||||
// Test clone to next layout
|
||||
const initialLayout = await client.evaluate(() => {
|
||||
return window.store.layout
|
||||
})
|
||||
await client.rightClick('.tabs .tab', 30, 20)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Clone to Next Layout")') // Click clone to next layout option
|
||||
await delay(2000)
|
||||
|
||||
// Verify new layout and tab count
|
||||
const newLayout = await client.evaluate(() => {
|
||||
return window.store.layout
|
||||
})
|
||||
expect(newLayout === initialLayout).equal(false) // Layout should have changed
|
||||
|
||||
const newTabsCount = await client.evaluate(() => {
|
||||
return window.store.tabs.length
|
||||
})
|
||||
expect(newTabsCount).equal(3) // Original + duplicated + cloned tab
|
||||
|
||||
// Restore original layout
|
||||
await client.evaluate((originalLayout) => {
|
||||
window.store.setLayout(originalLayout)
|
||||
}, initialLayout)
|
||||
await delay(1000)
|
||||
|
||||
// Verify layout was restored
|
||||
const finalLayout = await client.evaluate(() => {
|
||||
return window.store.layout
|
||||
})
|
||||
expect(finalLayout).equal(initialLayout)
|
||||
|
||||
await client.rightClick('.tabs .tab', 30, 20)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("New tab")')
|
||||
await delay(2000)
|
||||
|
||||
// Verify a new tab was created and is the last tab and active
|
||||
const tabsInfo = await client.evaluate(() => {
|
||||
const tabs = window.store.tabs
|
||||
return {
|
||||
totalCount: tabs.length,
|
||||
lastTabId: tabs[tabs.length - 1].id,
|
||||
activeTabId: window.store.activeTabId
|
||||
}
|
||||
})
|
||||
expect(tabsInfo.totalCount).equal(4)
|
||||
expect(tabsInfo.lastTabId).equal(tabsInfo.activeTabId)
|
||||
|
||||
await client.rightClick('.tabs .tab', 30, 20)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Close other tabs")')
|
||||
await delay(2000)
|
||||
|
||||
// Verify only one tab remains and it's the active tab
|
||||
const tabsInfoAfterClose = await client.evaluate(() => {
|
||||
const tabs = window.store.tabs
|
||||
return {
|
||||
totalCount: tabs.length,
|
||||
remainingTabId: tabs[0].id,
|
||||
activeTabId: window.store.activeTabId
|
||||
}
|
||||
})
|
||||
|
||||
expect(tabsInfoAfterClose.totalCount).equal(1)
|
||||
expect(tabsInfoAfterClose.remainingTabId).equal(tabsInfoAfterClose.activeTabId)
|
||||
|
||||
// close tab context menu
|
||||
// Create two more tabs for testing
|
||||
await client.click('.tabs-add-btn')
|
||||
await delay(500)
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(1000)
|
||||
|
||||
// Test closing a non-active tab
|
||||
const tabsBeforeClose = await client.evaluate(() => {
|
||||
return {
|
||||
count: window.store.tabs.length,
|
||||
activeTabId: window.store.activeTabId,
|
||||
secondTabId: window.store.tabs[1].id
|
||||
}
|
||||
})
|
||||
|
||||
await client.rightClick(`.tabs .tab[data-id="${tabsBeforeClose.secondTabId}"]`, 30, 20)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Close")')
|
||||
await delay(1000)
|
||||
|
||||
const tabsAfterNonActiveClose = await client.evaluate(() => {
|
||||
return {
|
||||
count: window.store.tabs.length,
|
||||
activeTabId: window.store.activeTabId
|
||||
}
|
||||
})
|
||||
|
||||
expect(tabsAfterNonActiveClose.count).equal(tabsBeforeClose.count - 1)
|
||||
expect(tabsAfterNonActiveClose.activeTabId).equal(tabsBeforeClose.activeTabId)
|
||||
|
||||
// Test closing the active tab
|
||||
const activeTabInfo = await client.evaluate(() => {
|
||||
return {
|
||||
activeTabId: window.store.activeTabId,
|
||||
nextTabId: window.store.tabs[0].id
|
||||
}
|
||||
})
|
||||
|
||||
await client.rightClick(`.tabs .tab[data-id="${activeTabInfo.activeTabId}"]`, 30, 20)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Close")')
|
||||
await delay(1000)
|
||||
|
||||
const tabsAfterActiveClose = await client.evaluate(() => {
|
||||
return {
|
||||
count: window.store.tabs.length,
|
||||
activeTabId: window.store.activeTabId
|
||||
}
|
||||
})
|
||||
|
||||
expect(tabsAfterActiveClose.count).equal(tabsAfterNonActiveClose.count - 1)
|
||||
expect(tabsAfterActiveClose.activeTabId).equal(activeTabInfo.nextTabId)
|
||||
|
||||
// Test rename tab from context menu
|
||||
// Create a new tab for rename testing
|
||||
await client.click('.tabs-add-btn')
|
||||
await delay(500)
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(1000)
|
||||
|
||||
const tabInfo = await client.evaluate(() => {
|
||||
return {
|
||||
tabId: window.store.tabs[0].id,
|
||||
originalTitle: window.store.tabs[0].title
|
||||
}
|
||||
})
|
||||
|
||||
// Test rename using Enter key
|
||||
await client.rightClick(`.tabs .tab[data-id="${tabInfo.tabId}"]`, 30, 20)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Rename")')
|
||||
await delay(500)
|
||||
|
||||
const newTitle1 = 'renamed-tab-1'
|
||||
await client.setValue('.tab input', newTitle1)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(500)
|
||||
|
||||
// Verify the title was changed and edit mode is off
|
||||
let renamedTabInfo = await client.evaluate(() => {
|
||||
const tab = window.store.tabs[0]
|
||||
return {
|
||||
title: tab.title,
|
||||
isEditing: !!tab.isEditting
|
||||
}
|
||||
})
|
||||
expect(renamedTabInfo.title).equal(newTitle1)
|
||||
expect(renamedTabInfo.isEditing).equal(false)
|
||||
|
||||
// Test rename by clicking outside
|
||||
await client.rightClick(`.tabs .tab[data-id="${tabInfo.tabId}"]`, 30, 20)
|
||||
await delay(500)
|
||||
await client.click('.ant-dropdown:not(.ant-dropdown-hidden) .ant-dropdown-menu-item:has-text("Rename")')
|
||||
await delay(500)
|
||||
|
||||
const newTitle2 = 'renamed-tab-2'
|
||||
await client.setValue('.tab input', newTitle2)
|
||||
await client.click('.tabs-add-btn') // Click outside to blur
|
||||
await delay(1200)
|
||||
|
||||
// Verify the title was changed and edit mode is off
|
||||
renamedTabInfo = await client.evaluate(() => {
|
||||
const tab = window.store.tabs[0]
|
||||
return {
|
||||
title: tab.title,
|
||||
isEditing: !!tab.isEditting
|
||||
}
|
||||
})
|
||||
expect(renamedTabInfo.title).equal(newTitle2)
|
||||
expect(renamedTabInfo.isEditing).equal(false)
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it, expect } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
const delay = require('./common/wait')
|
||||
const { nanoid } = require('nanoid')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('Local bookmark', function () {
|
||||
it('should create a local bookmark and verify history', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
|
||||
await delay(3500)
|
||||
|
||||
// Get initial history count
|
||||
const initialHistoryCount = await client.evaluate(() => {
|
||||
return window.store.history.length
|
||||
})
|
||||
|
||||
// Open bookmark form
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(500)
|
||||
|
||||
// Select local bookmark type
|
||||
await client.click('.setting-wrap .ant-radio-button-wrapper', 3)
|
||||
await delay(500)
|
||||
|
||||
// Generate a unique title for the bookmark
|
||||
const bookmarkTitle = `Local-${nanoid()}`
|
||||
const type = 'local'
|
||||
|
||||
// Fill in bookmark details
|
||||
await client.setValue('.setting-wrap input[id="local-form_title"]', bookmarkTitle)
|
||||
await client.setValue('.setting-wrap textarea[id="local-form_runScripts_0_script"]', 'ls')
|
||||
|
||||
// Save and connect
|
||||
await client.click('.setting-wrap .ant-btn-primary')
|
||||
await delay(2000)
|
||||
|
||||
// Verify that the history has been updated
|
||||
const historyItem = await client.evaluate(() => {
|
||||
const history = window.store.history
|
||||
return history[0]
|
||||
})
|
||||
|
||||
expect(historyItem.tab.title).toEqual(bookmarkTitle)
|
||||
expect(historyItem.tab.type).toEqual(type)
|
||||
|
||||
// Verify history count has increased
|
||||
const newHistoryCount = await client.evaluate(() => {
|
||||
return window.store.history.length
|
||||
})
|
||||
expect(newHistoryCount).toEqual(initialHistoryCount + 1)
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* trzsz / rzsz file-transfer test
|
||||
*
|
||||
* Tests all four transfer commands in a single SSH session:
|
||||
* • trz – trzsz upload (local → remote)
|
||||
* • tsz – trzsz download (remote → local)
|
||||
* • rz – rzsz upload (local → remote)
|
||||
* • sz – rzsz download (remote → local)
|
||||
*
|
||||
* Prerequisites on the remote host:
|
||||
* • trzsz package installed (provides trz / tsz)
|
||||
* • lrzsz package installed (provides rz / sz )
|
||||
*
|
||||
* Uses window._apiControlSelectFile to bypass the native file-picker dialog.
|
||||
* Uses window._apiControlSelectFolder to bypass the native folder-picker dialog.
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
|
||||
const os = require('os')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const { setupSshConnection, closeApp } = require('./common/common')
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Read the last `lines` lines of the active terminal via the store API. */
|
||||
async function getTerminalText (client, lines = 200) {
|
||||
const result = await client.evaluate((n) => {
|
||||
return window.store.mcpGetTerminalOutput({ lines: n })
|
||||
}, lines)
|
||||
return result ? result.output : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Count occurrences of `marker` in current terminal output.
|
||||
* Used as a baseline before issuing a transfer command so we can detect
|
||||
* when a *new* occurrence appears.
|
||||
*/
|
||||
async function countMarkerInTerminal (client, marker, lines = 200) {
|
||||
const content = await getTerminalText(client, lines)
|
||||
return (content.match(new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll terminal output until the count of `marker` exceeds `baseline`.
|
||||
* This lets us distinguish a *new* transfer completion from text left over
|
||||
* by a previous transfer.
|
||||
*/
|
||||
async function waitForNewMarker (client, marker, baseline, timeout = 60000) {
|
||||
const start = Date.now()
|
||||
while (Date.now() - start < timeout) {
|
||||
const count = await countMarkerInTerminal(client, marker)
|
||||
if (count > baseline) return true
|
||||
await delay(1500)
|
||||
}
|
||||
const lastContent = await getTerminalText(client)
|
||||
throw new Error(
|
||||
`Timeout (${timeout}ms) waiting for new "${marker}" (baseline ${baseline})\nLast terminal content:\n${lastContent}`
|
||||
)
|
||||
}
|
||||
|
||||
/** Type a shell command and press Enter in the active terminal. */
|
||||
async function runInTerminal (client, cmd) {
|
||||
await client.click('.session-current .term-wrap')
|
||||
await delay(400)
|
||||
await client.keyboard.type(cmd)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(500)
|
||||
}
|
||||
|
||||
// ─── transfer helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
async function testTrz (client, localFile) {
|
||||
log('020: testing trz (trzsz upload) …')
|
||||
const baseline = await countMarkerInTerminal(client, '[DONE]')
|
||||
await client.evaluate((file) => {
|
||||
window._apiControlSelectFile = [file]
|
||||
}, localFile)
|
||||
await runInTerminal(client, 'trz')
|
||||
await waitForNewMarker(client, '[DONE]', baseline, 60000)
|
||||
log('020: trz done')
|
||||
}
|
||||
|
||||
async function testTsz (client, remoteFile, saveDir) {
|
||||
log('020: testing tsz (trzsz download) …')
|
||||
const baseline = await countMarkerInTerminal(client, '[DONE]')
|
||||
await client.evaluate((dir) => {
|
||||
window._apiControlSelectFolder = dir
|
||||
}, saveDir)
|
||||
await runInTerminal(client, `tsz ${remoteFile}`)
|
||||
await waitForNewMarker(client, '[DONE]', baseline, 60000)
|
||||
// Verify at least one file arrived in the save dir
|
||||
const files = fs.readdirSync(saveDir)
|
||||
expect(files.length).greaterThan(0)
|
||||
log('020: tsz done')
|
||||
}
|
||||
|
||||
async function testRz (client, localFile) {
|
||||
log('020: testing rz (rzsz upload) …')
|
||||
const baseline = await countMarkerInTerminal(client, '[DONE]')
|
||||
await client.evaluate((file) => {
|
||||
window._apiControlSelectFile = [file]
|
||||
}, localFile)
|
||||
await runInTerminal(client, 'rz')
|
||||
await waitForNewMarker(client, '[DONE]', baseline, 60000)
|
||||
log('020: rz done')
|
||||
}
|
||||
|
||||
async function testSz (client, remoteFile, saveDir) {
|
||||
log('020: testing sz (rzsz download) …')
|
||||
const baseline = await countMarkerInTerminal(client, '[DONE]')
|
||||
await client.evaluate((dir) => {
|
||||
window._apiControlSelectFolder = dir
|
||||
}, saveDir)
|
||||
await runInTerminal(client, `sz ${remoteFile}`)
|
||||
await waitForNewMarker(client, '[DONE]', baseline, 60000)
|
||||
// Verify at least one file arrived in the save dir
|
||||
const files = fs.readdirSync(saveDir)
|
||||
expect(files.length).greaterThan(0)
|
||||
log('020: sz done')
|
||||
}
|
||||
|
||||
// ─── test ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('trzsz-rzsz-transfer', function () {
|
||||
it('should transfer files using trz / tsz / rz / sz', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
log('020: app launched')
|
||||
|
||||
await setupSshConnection(client)
|
||||
await delay(5000)
|
||||
log('020: SSH connected')
|
||||
|
||||
const timestamp = Date.now()
|
||||
const trzszTestFileName = `trzsz-upload-${timestamp}.txt`
|
||||
const rzszUploadFileName = `rzsz-upload-${timestamp}.txt`
|
||||
|
||||
const trzszLocalFile = path.join(os.tmpdir(), trzszTestFileName)
|
||||
const rzszLocalFile = path.join(os.tmpdir(), rzszUploadFileName)
|
||||
|
||||
const trzszSaveDir = path.join(os.tmpdir(), `trzsz-save-${timestamp}`)
|
||||
const rzszSaveDir = path.join(os.tmpdir(), `rzsz-save-${timestamp}`)
|
||||
|
||||
// Create local files used for uploads
|
||||
fs.writeFileSync(trzszLocalFile, `electerm trzsz test ${timestamp}`)
|
||||
fs.writeFileSync(rzszLocalFile, `electerm rzsz test ${timestamp}`)
|
||||
fs.mkdirSync(trzszSaveDir, { recursive: true })
|
||||
fs.mkdirSync(rzszSaveDir, { recursive: true })
|
||||
log(`020: local test files created: ${trzszLocalFile}, ${rzszLocalFile}`)
|
||||
|
||||
// ── trz ────────────────────────────────────────────────────────────────────
|
||||
await testTrz(client, trzszLocalFile)
|
||||
await delay(2000)
|
||||
|
||||
// ── tsz ────────────────────────────────────────────────────────────────────
|
||||
// Download the file that was just uploaded via trz
|
||||
const trzszRemoteFile = `~/${trzszTestFileName}`
|
||||
await testTsz(client, trzszRemoteFile, trzszSaveDir)
|
||||
await delay(2000)
|
||||
|
||||
// Create a remote-only file for sz to download (distinct from any upload)
|
||||
const rzszRemoteName = `rzsz-download-${timestamp}.txt`
|
||||
const rzszRemoteFile = `~/${rzszRemoteName}`
|
||||
await runInTerminal(client, `echo "rzsz download test ${timestamp}" > ${rzszRemoteFile}`)
|
||||
await delay(1500)
|
||||
|
||||
// ── rz ────────────────────────────────────────────────────────────────────
|
||||
// Upload a *different* file from a fresh local path so the remote does not
|
||||
// already have it (lrzsz crashes when it tries to skip an existing file).
|
||||
await testRz(client, rzszLocalFile)
|
||||
await delay(2000)
|
||||
|
||||
// ── sz ────────────────────────────────────────────────────────────────────
|
||||
await testSz(client, rzszRemoteFile, rzszSaveDir)
|
||||
await delay(2000)
|
||||
|
||||
// Cleanup local temp files
|
||||
try { fs.unlinkSync(trzszLocalFile) } catch (_) {}
|
||||
try { fs.unlinkSync(rzszLocalFile) } catch (_) {}
|
||||
|
||||
await closeApp(electronApp, __filename)
|
||||
log('020: all transfer tests passed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(100000)
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const { expect } = require('./common/expect')
|
||||
const appOptions = require('./common/app-options')
|
||||
const e = require('./common/lang')
|
||||
const extendClient = require('./common/client-extend')
|
||||
|
||||
describe('bookmark groups', function () {
|
||||
it('all buttons open proper bookmark tab', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(3500)
|
||||
|
||||
log('button:edit')
|
||||
await client.click('.btns .anticon-plus-circle')
|
||||
await delay(2500)
|
||||
const sel = '.setting-wrap .ant-tabs-nav-list .ant-tabs-tab-active'
|
||||
await client.hasElem(sel)
|
||||
const text = await client.getText(sel)
|
||||
expect(text).equal(e('bookmarks'))
|
||||
|
||||
log('click add category button')
|
||||
await client.click('.setting-wrap .anticon-folder.with-plus')
|
||||
|
||||
const id = 'u567'
|
||||
await client.setValue('.setting-wrap .item-list-wrap input.ant-input', id)
|
||||
|
||||
log('save it')
|
||||
const bookmarkGroupsCountPrev = await client.evaluate(() => {
|
||||
return window.store.bookmarkGroups.length
|
||||
})
|
||||
await delay(200)
|
||||
await client.click('.setting-wrap .ant-input-suffix .anticon-check')
|
||||
await delay(1200)
|
||||
const bookmarkGroupsCount = await client.evaluate(() => {
|
||||
return window.store.bookmarkGroups.length
|
||||
})
|
||||
expect(bookmarkGroupsCountPrev + 1).equal(bookmarkGroupsCount)
|
||||
await client.evaluate(() => {
|
||||
return window.store.setBookmarkGroups(
|
||||
window.store.bookmarkGroups.filter(d => d !== 'u567')
|
||||
)
|
||||
})
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* Terminal command-history test
|
||||
*
|
||||
* Verifies:
|
||||
* 1. Running a command in a local terminal adds it to cmd history.
|
||||
* 2. Clicking the history item re-runs the command.
|
||||
* 3. Deleting a history item removes it from the list.
|
||||
* 4. History persists across app restarts.
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const { closeApp } = require('./common/common')
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Open the command-history popover in the footer.
|
||||
* Closes any currently-open popover first so we get a clean open. */
|
||||
async function openHistoryPopover (client) {
|
||||
// Click outside any popover to ensure it is closed before reopening
|
||||
await client.click('.session-current .term-wrap')
|
||||
await delay(400)
|
||||
await client.click('.terminal-footer-history .ant-btn')
|
||||
await delay(800)
|
||||
}
|
||||
|
||||
/** Close the popover by clicking outside of it. */
|
||||
async function closeHistoryPopover (client) {
|
||||
await client.click('.session-current .term-wrap')
|
||||
await delay(400)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the text of all visible command-history items.
|
||||
* The popover must already be open.
|
||||
*/
|
||||
async function getHistoryItems (client) {
|
||||
const items = await client.locator('.cmd-history-item .cmd-history-item-text')
|
||||
const count = await items.count()
|
||||
const texts = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
texts.push(await items.nth(i).innerText())
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
/**
|
||||
* Hover over a history item whose text includes `term` to reveal action buttons,
|
||||
* then click the delete button. The popover must already be open.
|
||||
*/
|
||||
async function deleteHistoryItem (client, term) {
|
||||
const items = client.locator('.cmd-history-item')
|
||||
const count = await items.count()
|
||||
for (let i = 0; i < count; i++) {
|
||||
const txt = await items.nth(i).locator('.cmd-history-item-text').innerText()
|
||||
if (txt.includes(term)) {
|
||||
await items.nth(i).hover()
|
||||
await delay(400)
|
||||
await items.nth(i).locator('.cmd-history-item-delete').click()
|
||||
await delay(600)
|
||||
return
|
||||
}
|
||||
}
|
||||
throw new Error(`History item matching "${term}" not found in popover`)
|
||||
}
|
||||
|
||||
/** Return the cmd strings currently in window.store.terminalCommandHistory. */
|
||||
async function getStoredHistory (client) {
|
||||
return client.evaluate(() =>
|
||||
window.store.terminalCommandHistory.map(i => i.cmd)
|
||||
)
|
||||
}
|
||||
|
||||
/** Type a command in the terminal and press Enter. */
|
||||
async function runInTerminal (client, cmd) {
|
||||
await client.click('.session-current .term-wrap')
|
||||
await delay(1000)
|
||||
await client.keyboard.type(cmd)
|
||||
await delay(300)
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(1500)
|
||||
}
|
||||
|
||||
// ─── test ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('cmd-history', function () {
|
||||
it('should add, re-run, delete commands and persist across restarts', async function () {
|
||||
// ── first session ──────────────────────────────────────────────────────────
|
||||
let electronApp = await electron.launch(appOptions)
|
||||
let client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(13000)
|
||||
log('021: app launched (session 1)')
|
||||
|
||||
const ts = Date.now()
|
||||
// Use simple commands without quotes to avoid shell-escaping issues
|
||||
const cmd1 = `echo cmd-history-test-${ts}`
|
||||
const cmd2 = `echo cmd-history-persist-${ts}`
|
||||
|
||||
// Clear existing history so our unique commands are easy to find
|
||||
await client.evaluate(() => window.store.clearAllCmdHistory())
|
||||
await delay(300)
|
||||
|
||||
// ── step 1: run cmd1 in terminal → should appear in history ───────────────
|
||||
await runInTerminal(client, cmd1)
|
||||
log(`021: ran "${cmd1}" in terminal`)
|
||||
|
||||
// Verify via store (ground truth)
|
||||
const stored1 = await getStoredHistory(client)
|
||||
log(`021: stored history: ${JSON.stringify(stored1)}`)
|
||||
expect(stored1.includes(cmd1)).equal(true)
|
||||
|
||||
// Verify via UI popover
|
||||
await openHistoryPopover(client)
|
||||
const items1 = await getHistoryItems(client)
|
||||
log(`021: history popover items: ${JSON.stringify(items1)}`)
|
||||
expect(items1.some(t => t.trim() === cmd1 || t.includes(`cmd-history-test-${ts}`))).equal(true)
|
||||
|
||||
// ── step 2: click the item to re-run the command ──────────────────────────
|
||||
const allItems = client.locator('.cmd-history-item')
|
||||
const count = await allItems.count()
|
||||
let clicked = false
|
||||
for (let i = 0; i < count; i++) {
|
||||
const txt = await allItems.nth(i).locator('.cmd-history-item-text').innerText()
|
||||
if (txt.includes(`cmd-history-test-${ts}`)) {
|
||||
await allItems.nth(i).locator('.cmd-history-item-text').click()
|
||||
clicked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
expect(clicked).equal(true)
|
||||
await delay(2000)
|
||||
log('021: clicked history item to re-run')
|
||||
|
||||
// count in store should have updated (command was re-run)
|
||||
const stored2 = await getStoredHistory(client)
|
||||
expect(stored2.includes(cmd1)).equal(true)
|
||||
|
||||
// ── step 3: delete the item ────────────────────────────────────────────────
|
||||
// Reopen popover (clicking the item might or might not have closed it; use
|
||||
// the helper which first clicks outside to normalise state)
|
||||
await openHistoryPopover(client)
|
||||
await deleteHistoryItem(client, `cmd-history-test-${ts}`)
|
||||
const storedAfterDel = await getStoredHistory(client)
|
||||
log(`021: history after delete: ${JSON.stringify(storedAfterDel)}`)
|
||||
expect(storedAfterDel.includes(cmd1)).equal(false)
|
||||
await closeHistoryPopover(client)
|
||||
|
||||
// ── step 4: run cmd2 that should persist after restart ────────────────────
|
||||
await runInTerminal(client, cmd2)
|
||||
await delay(2000) // allow store + db flush
|
||||
log(`021: ran "${cmd2}" in terminal`)
|
||||
|
||||
await closeApp(electronApp, __filename)
|
||||
log('021: session 1 closed')
|
||||
|
||||
// ── second session (restart) ───────────────────────────────────────────────
|
||||
await delay(2000)
|
||||
electronApp = await electron.launch(appOptions)
|
||||
client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await delay(13000)
|
||||
log('021: app launched (session 2 – restart)')
|
||||
|
||||
// cmd2 must still be in history
|
||||
const storedRestart = await getStoredHistory(client)
|
||||
log(`021: history after restart: ${JSON.stringify(storedRestart)}`)
|
||||
expect(storedRestart.includes(cmd2)).equal(true)
|
||||
|
||||
// cmd1 must NOT be in history (was deleted before restart)
|
||||
expect(storedRestart.includes(cmd1)).equal(false)
|
||||
|
||||
await closeApp(electronApp, __filename)
|
||||
log('021: all cmd-history tests passed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Session (connection) history test
|
||||
*
|
||||
* Verifies:
|
||||
* 1. Opening a new SSH session adds it to the sidebar history panel.
|
||||
* 2. Clicking a history item opens a new session tab.
|
||||
* 3. Deleting a history item removes it from the list.
|
||||
* 4. Clicking the bookmark icon on a history item creates a bookmark.
|
||||
* 5. History persists across app restarts.
|
||||
* 6. "Sort by frequency" re-orders the history list.
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const { setupSshConnection, closeApp } = require('./common/common')
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Open the sidebar and switch to the "history" tab. */
|
||||
async function openSidebarHistory (client) {
|
||||
await client.evaluate(() => {
|
||||
window.store.setOpenedSideBar('bookmarks')
|
||||
window.store.handleSidebarPanelTab('history')
|
||||
})
|
||||
await delay(600)
|
||||
}
|
||||
|
||||
/** Close the sidebar. */
|
||||
async function closeSidebar (client) {
|
||||
await client.evaluate(() => window.store.setOpenedSideBar(''))
|
||||
await delay(300)
|
||||
}
|
||||
|
||||
/** Return the raw history array from the store. */
|
||||
async function getHistoryStore (client) {
|
||||
return client.evaluate(() =>
|
||||
window.store.history.map(h => ({ id: h.id, count: h.count, host: h.tab && h.tab.host }))
|
||||
)
|
||||
}
|
||||
|
||||
/** Return the number of visible history items in the sidebar. */
|
||||
async function getHistoryItemCount (client) {
|
||||
return client.locator('.sidebar-panel-history .item-list-unit').count()
|
||||
}
|
||||
|
||||
/** Return the current tab count. */
|
||||
async function getTabCount (client) {
|
||||
return client.locator('.tabs .tabs-wrapper .tab').count()
|
||||
}
|
||||
|
||||
// ─── test ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('session-history', function () {
|
||||
it('should track, open, delete, bookmark history, persist and sort by frequency', async function () {
|
||||
// ── first session ──────────────────────────────────────────────────────────
|
||||
let electronApp = await electron.launch(appOptions)
|
||||
let client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
// Wait until initData has completed AND the history watcher is active
|
||||
await client.waitForFunction(() => {
|
||||
return window.store &&
|
||||
window.store.configLoaded === true &&
|
||||
!!window.watchhistory
|
||||
}, { timeout: 15000 })
|
||||
await delay(500) // small buffer after watcher starts
|
||||
log('022: app launched (session 1)')
|
||||
|
||||
// Clear existing connection history
|
||||
await client.evaluate(() => { window.store.history.splice(0, window.store.history.length) })
|
||||
await delay(500) // allow watcher to flush removes to DB
|
||||
|
||||
// ── step 1: open SSH session → history entry created ──────────────────────
|
||||
await setupSshConnection(client)
|
||||
await delay(4000)
|
||||
log('022: SSH connected')
|
||||
|
||||
// Add a synthetic second entry so the delete step (step 5) leaves one entry
|
||||
// for the persistence check (step 7). Uses unshift so it becomes index 0.
|
||||
await client.evaluate(() => {
|
||||
window.store.history.unshift({
|
||||
id: 'synth-' + Date.now(),
|
||||
count: 5,
|
||||
time: Date.now() - 5000,
|
||||
tab: { host: '10.0.0.1', type: 'ssh', username: 'synthuser', port: 22 }
|
||||
})
|
||||
})
|
||||
await delay(800) // let watcher persist the synthetic entry
|
||||
|
||||
const history1 = await getHistoryStore(client)
|
||||
log(`022: history after SSH connect: ${JSON.stringify(history1)}`)
|
||||
expect(history1.length).greaterThan(0)
|
||||
|
||||
// ── step 2: open sidebar history → item visible ────────────────────────────
|
||||
await openSidebarHistory(client)
|
||||
const historyCount1 = await getHistoryItemCount(client)
|
||||
log(`022: sidebar history item count: ${historyCount1}`)
|
||||
expect(historyCount1).greaterThan(0)
|
||||
|
||||
// ── step 3: click history item → new tab opens ────────────────────────────
|
||||
const tabsBefore = await getTabCount(client)
|
||||
await client.click('.sidebar-panel-history .item-list-unit', 0)
|
||||
await delay(3000)
|
||||
const tabsAfter = await getTabCount(client)
|
||||
log(`022: tabs before=${tabsBefore} after=${tabsAfter}`)
|
||||
expect(tabsAfter).greaterThan(tabsBefore)
|
||||
|
||||
// ── step 4: bookmark icon → creates a bookmark ────────────────────────────
|
||||
await openSidebarHistory(client)
|
||||
const bookmarksBefore = await client.evaluate(() => window.store.bookmarks.length)
|
||||
|
||||
// Hover first item to reveal the hidden bookmark icon, then click it
|
||||
const firstHistoryItem = client.locator('.sidebar-panel-history .item-list-unit').first()
|
||||
await firstHistoryItem.hover()
|
||||
await delay(200)
|
||||
await firstHistoryItem.locator('.list-item-bookmark').click()
|
||||
await delay(800)
|
||||
log('022: clicked bookmark icon on history item')
|
||||
|
||||
// A modal should appear – confirm it (custom modal uses .custom-modal-footer-buttons)
|
||||
const confirmBtn = client.locator('.custom-modal-footer-buttons .ant-btn-primary')
|
||||
await confirmBtn.waitFor({ state: 'visible', timeout: 8000 })
|
||||
await confirmBtn.click()
|
||||
await delay(1000)
|
||||
log('022: confirmed bookmark creation')
|
||||
|
||||
const bookmarksAfter = await client.evaluate(() => window.store.bookmarks.length)
|
||||
log(`022: bookmarks before=${bookmarksBefore} after=${bookmarksAfter}`)
|
||||
expect(bookmarksAfter).greaterThan(bookmarksBefore)
|
||||
|
||||
// ── step 5: delete a history item ─────────────────────────────────────────
|
||||
await openSidebarHistory(client)
|
||||
const countBefore = await getHistoryItemCount(client)
|
||||
const firstHistoryItem2 = client.locator('.sidebar-panel-history .item-list-unit').first()
|
||||
await firstHistoryItem2.hover()
|
||||
await delay(200)
|
||||
await firstHistoryItem2.locator('.list-item-edit').click()
|
||||
await delay(800)
|
||||
const countAfter = await getHistoryItemCount(client)
|
||||
log(`022: sidebar history count before=${countBefore} after=${countAfter}`)
|
||||
expect(countAfter).equal(countBefore - 1)
|
||||
|
||||
// ── step 6: sort by frequency ─────────────────────────────────────────────
|
||||
await closeSidebar(client)
|
||||
await openSidebarHistory(client)
|
||||
|
||||
// Toggle "sort by frequency" switch
|
||||
const switchSel = '.sidebar-panel-history .history-header .ant-switch'
|
||||
const sortEnabled1 = await client.evaluate(() => {
|
||||
// Read localStorage key used by history.jsx
|
||||
const v = window.localStorage.getItem('electerm-history-sort-by-frequency')
|
||||
return v === 'true'
|
||||
})
|
||||
await client.locator(switchSel).click({ force: true })
|
||||
await delay(500)
|
||||
const sortEnabled2 = await client.evaluate(() => {
|
||||
const v = window.localStorage.getItem('electerm-history-sort-by-frequency')
|
||||
return v === 'true'
|
||||
})
|
||||
log(`022: sort-by-freq toggled: ${sortEnabled1} → ${sortEnabled2}`)
|
||||
expect(sortEnabled2).equal(!sortEnabled1)
|
||||
|
||||
// Toggle back
|
||||
await client.locator(switchSel).click({ force: true })
|
||||
await delay(300)
|
||||
|
||||
// ── step 7: close & reopen (persistence check) ────────────────────────────
|
||||
const historyBeforeClose = await getHistoryStore(client)
|
||||
await closeSidebar(client)
|
||||
await delay(2000) // ensure DB flushes before close
|
||||
await closeApp(electronApp, __filename)
|
||||
log('022: session 1 closed')
|
||||
|
||||
await delay(2000)
|
||||
electronApp = await electron.launch(appOptions)
|
||||
client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
await client.waitForFunction(() => {
|
||||
return window.store &&
|
||||
window.store.configLoaded === true &&
|
||||
!!window.watchhistory
|
||||
}, { timeout: 15000 })
|
||||
await delay(300)
|
||||
log('022: app launched (session 2 – restart)')
|
||||
|
||||
const historyAfterRestart = await getHistoryStore(client)
|
||||
log(`022: history after restart: ${JSON.stringify(historyAfterRestart)}`)
|
||||
// At least the remaining entry should be there
|
||||
expect(historyAfterRestart.length).greaterThan(0)
|
||||
// All IDs present before close must still be there (minus the deleted one)
|
||||
for (const h of historyAfterRestart) {
|
||||
const found = historyBeforeClose.some(p => p.id === h.id)
|
||||
expect(found).equal(true)
|
||||
}
|
||||
|
||||
await closeApp(electronApp, __filename)
|
||||
log('022: all session-history tests passed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Batch Operation e2e test
|
||||
*
|
||||
* Verifies:
|
||||
* 1. Widgets panel opens with the Batch Operation widget
|
||||
* 2. Editor is displayed with "Execute Workflow" button
|
||||
* 3. Load Template populates the editor
|
||||
* 4. Executing the full example workflow (connect + command + sftp_download +
|
||||
* sftp_upload + cleanup) shows progress in the log div
|
||||
* 5. Log entries appear for each step (success / error)
|
||||
*/
|
||||
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const { test: it } = require('@playwright/test')
|
||||
const { describe } = it
|
||||
it.setTimeout(10000000)
|
||||
|
||||
const delay = require('./common/wait')
|
||||
const log = require('./common/log')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { expect } = require('./common/expect')
|
||||
const batchOpExample = require('./common/batch-op-example')
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Open the Widgets settings panel via the store API */
|
||||
async function openWidgetsPanel (client) {
|
||||
await client.evaluate(() => window.store.openWidgetsModal())
|
||||
await delay(800)
|
||||
}
|
||||
|
||||
/** Click the first widget whose list-item title contains `name` */
|
||||
async function clickWidgetByName (client, name) {
|
||||
const item = client.locator(`.item-list-unit .list-item-title:has-text("${name}")`)
|
||||
await item.first().click()
|
||||
await delay(500)
|
||||
}
|
||||
|
||||
/** Return the current text content of the batch-op log div */
|
||||
async function getLogText (client) {
|
||||
const el = client.locator('.batch-op-logs')
|
||||
const count = await el.count()
|
||||
if (count === 0) return ''
|
||||
return el.first().innerText()
|
||||
}
|
||||
|
||||
/** Inject workflow JSON into the editor textarea via React synthetic event */
|
||||
async function setWorkflow (client, workflow) {
|
||||
await client.evaluate((json) => {
|
||||
const ta = document.querySelector('.batch-op-editor textarea')
|
||||
if (ta) {
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
|
||||
nativeInputValueSetter.call(ta, json)
|
||||
ta.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
}
|
||||
}, JSON.stringify(workflow, null, 2))
|
||||
await delay(400)
|
||||
}
|
||||
|
||||
// ─── tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('batch-op', function () {
|
||||
it('should run full example workflow and show log entries for every step', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
|
||||
await client.waitForFunction(() => window.store && window.store.configLoaded === true, { timeout: 15000 })
|
||||
await delay(500)
|
||||
log('023-batch-op: app launched')
|
||||
|
||||
// ── open widgets panel ─────────────────────────────────────────────────
|
||||
await openWidgetsPanel(client)
|
||||
log('023-batch-op: widgets panel opened')
|
||||
|
||||
// ── click the Batch Operation widget ──────────────────────────────────
|
||||
await clickWidgetByName(client, 'Batch Operation')
|
||||
await delay(600)
|
||||
log('023-batch-op: Batch Operation widget selected')
|
||||
|
||||
// Editor and Execute button should be visible
|
||||
const editorVisible = await client.locator('.batch-op-editor').count()
|
||||
expect(editorVisible).greaterThan(0)
|
||||
const execBtn = client.locator('button:has-text("Execute Workflow")')
|
||||
expect(await execBtn.count()).greaterThan(0)
|
||||
log('023-batch-op: editor and execute button visible')
|
||||
|
||||
// ── Load Template and verify editor populated ──────────────────────────
|
||||
await client.click('button:has-text("Load Template")')
|
||||
await delay(400)
|
||||
const editorVal = await client.evaluate(() => {
|
||||
const ta = document.querySelector('.batch-op-editor textarea')
|
||||
return ta ? ta.value : ''
|
||||
})
|
||||
expect(editorVal.length).greaterThan(10)
|
||||
log('023-batch-op: template loaded into editor')
|
||||
|
||||
// ── set the full example workflow (connect + commands + sftp) ──────────
|
||||
await setWorkflow(client, batchOpExample)
|
||||
log('023-batch-op: example workflow JSON set')
|
||||
|
||||
// ── execute workflow ───────────────────────────────────────────────────
|
||||
await execBtn.first().click()
|
||||
log('023-batch-op: execute clicked, waiting for all steps...')
|
||||
|
||||
// Wait until all steps complete (log div has success/error entries)
|
||||
// Workflow has 7 steps; allow up to 3 minutes for sftp transfers
|
||||
await client.waitForFunction(() => {
|
||||
const entries = document.querySelectorAll('.batch-op-log-entry')
|
||||
return entries.length >= 7
|
||||
}, { timeout: 180000 })
|
||||
await delay(1000)
|
||||
|
||||
const logText = await getLogText(client)
|
||||
log(`023-batch-op: log content:\n${logText}`)
|
||||
|
||||
// Every step should have a success entry
|
||||
const successCount = await client.locator('.batch-op-log-entry.success').count()
|
||||
expect(successCount).greaterThan(0)
|
||||
log(`023-batch-op: ${successCount} success log entries found`)
|
||||
|
||||
// There should be no error entries
|
||||
const errorCount = await client.locator('.batch-op-log-entry.error').count()
|
||||
expect(errorCount).equal(0)
|
||||
log('023-batch-op: no error entries — all steps passed')
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
|
||||
it('should show error log entry when a step fails', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
|
||||
await client.waitForFunction(() => window.store && window.store.configLoaded === true, { timeout: 15000 })
|
||||
await delay(500)
|
||||
|
||||
await openWidgetsPanel(client)
|
||||
await clickWidgetByName(client, 'Batch Operation')
|
||||
await delay(600)
|
||||
|
||||
// Use a bad host so the connect step fails immediately
|
||||
const workflow = [
|
||||
{
|
||||
name: 'Bad Connect',
|
||||
action: 'connect',
|
||||
params: {
|
||||
host: '192.0.2.1', // TEST-NET – guaranteed unreachable
|
||||
port: 22,
|
||||
username: 'nobody',
|
||||
authType: 'password',
|
||||
password: 'wrong'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
await setWorkflow(client, workflow)
|
||||
|
||||
const execBtn = client.locator('button:has-text("Execute Workflow")')
|
||||
await execBtn.first().click()
|
||||
log('023-batch-op: error-test execute clicked')
|
||||
|
||||
// Wait for an error entry (up to ~35 s for connection timeout)
|
||||
await client.waitForFunction(() => {
|
||||
const entries = document.querySelectorAll('.batch-op-log-entry.error')
|
||||
return entries.length > 0
|
||||
}, { timeout: 35000 })
|
||||
|
||||
const errorCount = await client.locator('.batch-op-log-entry.error').count()
|
||||
expect(errorCount).greaterThan(0)
|
||||
log(`023-batch-op: ${errorCount} error log entries found`)
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
const { _electron: electron } = require('@playwright/test')
|
||||
const {
|
||||
test: it,
|
||||
expect
|
||||
} = require('@playwright/test')
|
||||
const { describe } = it
|
||||
const delay = require('./common/wait')
|
||||
const appOptions = require('./common/app-options')
|
||||
const extendClient = require('./common/client-extend')
|
||||
const { getTerminalContent } = require('./common/basic-terminal-test')
|
||||
|
||||
describe('batch input', function () {
|
||||
it('should execute ls command in selected tabs', async function () {
|
||||
const electronApp = await electron.launch(appOptions)
|
||||
const client = await electronApp.firstWindow()
|
||||
extendClient(client, electronApp)
|
||||
|
||||
// Wait for app to load
|
||||
await delay(3500)
|
||||
|
||||
// Create a new tab first so we have 2 tabs
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(500)
|
||||
await client.click('.add-menu-wrap .context-item:has-text("New tab")')
|
||||
await client.click('.tabs .tabs-add-btn')
|
||||
await delay(1500)
|
||||
|
||||
// Get initial tab count
|
||||
const tabsCount = await client.countElem('.tabs .tab')
|
||||
expect(tabsCount).toEqual(2)
|
||||
|
||||
const text1 = await getTerminalContent(client)
|
||||
|
||||
// Switch to second tab
|
||||
// await client.click('.tabs .tab', 0)
|
||||
// await delay(1000)
|
||||
const text2 = text1
|
||||
|
||||
// Enter command in batch input
|
||||
await client.click('.batch-input-holder')
|
||||
await delay(1500)
|
||||
|
||||
// Enter ls command
|
||||
await client.setValue('.terminal-footer-flex .bi-full .ant-input', 'ls')
|
||||
await delay(1500)
|
||||
|
||||
await client.keyboard.press('Enter')
|
||||
await delay(1000)
|
||||
|
||||
// Check first tab output
|
||||
await client.click('.tabs .tab', 0)
|
||||
await delay(1000)
|
||||
const newText1 = await getTerminalContent(client)
|
||||
|
||||
// Check second tab output
|
||||
await client.click('.tabs .tab', 1)
|
||||
await delay(1000)
|
||||
const newText2 = await getTerminalContent(client)
|
||||
|
||||
// Verify command output in both terminals
|
||||
expect(newText1.length === text1.length).toEqual(true)
|
||||
expect(newText2.length).toBeGreaterThan(text2.length)
|
||||
|
||||
// Check that ls command history is saved
|
||||
const batchHistory = await client.evaluate(() => {
|
||||
return window.store.batchInputs
|
||||
})
|
||||
expect(batchHistory).toContain('ls')
|
||||
|
||||
await electronApp.close().catch(console.log)
|
||||
})
|
||||
})
|
||||
@@ -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