chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:46:20 +08:00
commit eb21b65815
830 changed files with 113645 additions and 0 deletions
+93
View File
@@ -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)
})
})
+132
View File
@@ -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)
})
})
+68
View File
@@ -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)
})
})
+67
View File
@@ -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)
})
})
+47
View File
@@ -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)
})
+56
View File
@@ -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)
})
+128
View File
@@ -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()
}
})
})
+35
View File
@@ -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)
})
})
+81
View File
@@ -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()
})
})
+82
View File
@@ -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)
})
})
+88
View File
@@ -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)
})
})
+74
View File
@@ -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)
})
})
+65
View File
@@ -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)
})
})
+30
View File
@@ -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)
})
})
+71
View File
@@ -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)
})
})
+64
View File
@@ -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)
})
})
+60
View File
@@ -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)
})
})
+64
View File
@@ -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)
})
})
+30
View File
@@ -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)
}
+180
View File
@@ -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()
})
})
+102
View File
@@ -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)
}
+55
View File
@@ -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)
})
})
+169
View File
@@ -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)
}
+78
View File
@@ -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')
})
})
+187
View File
@@ -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)
})
})
+250
View File
@@ -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)
}
+240
View File
@@ -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)
}
+196
View File
@@ -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}`)
}
}
+195
View File
@@ -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)
}
+250
View File
@@ -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)
}
+156
View File
@@ -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
}
+140
View File
@@ -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')
})
})
+61
View File
@@ -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')
})
})
+104
View File
@@ -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)
})
})
+79
View File
@@ -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)
})
})
+38
View File
@@ -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()
})
})
+213
View File
@@ -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)
})
})
+59
View File
@@ -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)
})
})
+197
View File
@@ -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)
})
})
+184
View File
@@ -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')
})
})
+206
View File
@@ -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')
})
})
+176
View File
@@ -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)
})
})
+72
View File
@@ -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)
})
})
+147
View File
@@ -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}`)
})
+14
View File
@@ -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'
]
}
+42
View File
@@ -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
}
+77
View File
@@ -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"
}
]
+101
View File
@@ -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
}
+465
View File
@@ -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
}
+30
View File
@@ -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
}
+33
View File
@@ -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)
}
+67
View File
@@ -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)
})
})
+10
View File
@@ -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
}
+14
View File
@@ -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)
}
+4
View File
@@ -0,0 +1,4 @@
module.exports = (...arg) => {
const timestamp = new Date().toISOString()
console.log(`[${timestamp}] >`, ...arg)
}
+10
View File
@@ -0,0 +1,10 @@
/**
* output uid with os name prefix
*/
const { nanoid } = require('nanoid')
const os = require('os').platform()
module.exports = () => {
return os + '_' + nanoid()
}
+5
View File
@@ -0,0 +1,5 @@
/**
* wait async
* @param {*} time
*/
module.exports = time => new Promise(resolve => setTimeout(resolve, time))