Compare commits
2 Commits
8e0c37bf74
...
6f19890a84
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f19890a84 | ||
|
|
1ee33672dd |
14
package-lock.json
generated
14
package-lock.json
generated
@@ -41,6 +41,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"autoprefixer": "^10.4.27",
|
"autoprefixer": "^10.4.27",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
"electron": "^39.2.6",
|
"electron": "^39.2.6",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
@@ -5591,6 +5592,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||||
|
"integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv-expand": {
|
"node_modules/dotenv-expand": {
|
||||||
"version": "11.0.7",
|
"version": "11.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"@vitejs/plugin-react": "^5.1.1",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"autoprefixer": "^10.4.27",
|
"autoprefixer": "^10.4.27",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
"electron": "^39.2.6",
|
"electron": "^39.2.6",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
@@ -74,9 +75,9 @@
|
|||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
|
"tsx": "^4.19.3",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.2.6",
|
"vite": "^7.2.6",
|
||||||
"vitest": "^4.0.18",
|
"vitest": "^4.0.18"
|
||||||
"tsx": "^4.19.3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { chromium, type BrowserContext, type Page } from 'playwright'
|
import { chromium } from 'playwright'
|
||||||
import type { ErpConfig, ErpSession } from '../../types/erp.types'
|
import type { ErpConfig, ErpSession } from '../../types/erp.types'
|
||||||
import { createLogger } from '../logger'
|
import { createLogger } from '../logger'
|
||||||
|
|
||||||
const log = createLogger('ErpAuthService')
|
const log = createLogger('ErpAuthService')
|
||||||
|
|
||||||
|
// Timeout constants
|
||||||
|
const PAGE_LOAD_TIMEOUT = 10000
|
||||||
|
const LOGIN_RESULT_TIMEOUT = 15000
|
||||||
|
const FORCE_LOGIN_TIMEOUT = 5000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ERP Authentication Service
|
* ERP Authentication Service
|
||||||
* Manages login session and browser lifecycle
|
* Manages login session and browser lifecycle
|
||||||
@@ -51,10 +56,13 @@ export class ErpAuthService {
|
|||||||
await page.goto(loginUrl)
|
await page.goto(loginUrl)
|
||||||
|
|
||||||
// Wait for page to load
|
// Wait for page to load
|
||||||
await page.waitForLoadState('domcontentloaded', { timeout: 10000 })
|
await page.waitForLoadState('domcontentloaded', { timeout: PAGE_LOAD_TIMEOUT })
|
||||||
|
|
||||||
// Wait for iframe to be present
|
// Wait for iframe to be present
|
||||||
await page.waitForSelector('#forwardFrame', { state: 'attached', timeout: 15000 })
|
await page.waitForSelector('#forwardFrame', {
|
||||||
|
state: 'attached',
|
||||||
|
timeout: LOGIN_RESULT_TIMEOUT
|
||||||
|
})
|
||||||
|
|
||||||
// Extract forwardFrame (Python: main_frame = page.locator("#forwardFrame").content_frame)
|
// Extract forwardFrame (Python: main_frame = page.locator("#forwardFrame").content_frame)
|
||||||
// This is the main working frame for all subsequent operations
|
// This is the main working frame for all subsequent operations
|
||||||
@@ -89,29 +97,11 @@ export class ErpAuthService {
|
|||||||
throw new Error(`Failed to click login button: ${e}`)
|
throw new Error(`Failed to click login button: ${e}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for navigation after login
|
await page.waitForLoadState('domcontentloaded', { timeout: PAGE_LOAD_TIMEOUT }).catch(() => {
|
||||||
// The login will redirect to the main page which has a different structure
|
|
||||||
try {
|
|
||||||
await page.waitForLoadState('domcontentloaded', { timeout: 10000 })
|
|
||||||
} catch (e) {
|
|
||||||
log.warn('Page load state check timed out, continuing')
|
log.warn('Page load state check timed out, continuing')
|
||||||
}
|
})
|
||||||
|
|
||||||
// Handle force login confirmation dialog if present (Python: get_by_role("button", name="确定"))
|
await this.waitForLoginResult(mainFrame as unknown as import('playwright').Frame)
|
||||||
try {
|
|
||||||
const confirmBtn = mainFrame.getByRole('button', { name: '确定' })
|
|
||||||
const count = await confirmBtn.count()
|
|
||||||
if (count > 0) {
|
|
||||||
log.info('Force login detected, clicking confirm button')
|
|
||||||
await confirmBtn.first().click()
|
|
||||||
await page.waitForTimeout(2000)
|
|
||||||
} else {
|
|
||||||
log.debug('Normal login, no confirmation dialog')
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// No force login dialog, continue
|
|
||||||
log.debug('Normal login, no confirmation dialog')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create session with mainFrame (Python returns main_frame as part of login result)
|
// Create session with mainFrame (Python returns main_frame as part of login result)
|
||||||
this.session = {
|
this.session = {
|
||||||
@@ -126,6 +116,47 @@ export class ErpAuthService {
|
|||||||
return this.session
|
return this.session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for login result: success, failure, or force login confirmation
|
||||||
|
*/
|
||||||
|
private async waitForLoginResult(mainFrame: import('playwright').Frame): Promise<void> {
|
||||||
|
const successLocator = mainFrame.locator('.nc-workbench-icon')
|
||||||
|
const errorLocator = mainFrame.getByText('名称或密码错误')
|
||||||
|
const forceLoginButton = mainFrame.getByRole('button', { name: '确定' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
successLocator.waitFor({ state: 'visible', timeout: LOGIN_RESULT_TIMEOUT }),
|
||||||
|
errorLocator.waitFor({ state: 'visible', timeout: LOGIN_RESULT_TIMEOUT }),
|
||||||
|
forceLoginButton
|
||||||
|
.waitFor({ state: 'visible', timeout: FORCE_LOGIN_TIMEOUT })
|
||||||
|
.then(async () => {
|
||||||
|
log.info('Force login dialog detected, clicking confirm')
|
||||||
|
await forceLoginButton.click()
|
||||||
|
await this.waitForLoginResult(mainFrame)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
const hasError = await errorLocator.isVisible()
|
||||||
|
if (hasError) {
|
||||||
|
throw new Error('ERP 登录失败:名称或密码错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Login successful')
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes('名称或密码错误')) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasError = await errorLocator.isVisible().catch(() => false)
|
||||||
|
if (hasError) {
|
||||||
|
throw new Error('ERP 登录失败:名称或密码错误')
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Login successful')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close browser and cleanup session
|
* Close browser and cleanup session
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
import { beforeAll, afterAll } from 'vitest'
|
import { beforeAll, afterAll, vi } from 'vitest'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
|
// Mock electron app module for unit tests
|
||||||
|
vi.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
isPackaged: false,
|
||||||
|
isReady: vi.fn().mockReturnValue(false),
|
||||||
|
getPath: vi.fn().mockReturnValue(path.join(process.cwd(), 'logs')),
|
||||||
|
on: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
// Load environment variables from project root
|
// Load environment variables from project root
|
||||||
dotenv.config({ path: path.resolve(process.cwd(), '.env') })
|
dotenv.config({ path: path.resolve(process.cwd(), '.env') })
|
||||||
|
|
||||||
|
|||||||
@@ -56,4 +56,38 @@ describe('ERP Authentication Service (Unit)', () => {
|
|||||||
await expect(service.close()).resolves.toBeUndefined()
|
await expect(service.close()).resolves.toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Class Structure', () => {
|
||||||
|
let service: ErpAuthService
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const config: ErpConfig = {
|
||||||
|
url: 'https://test.example.com',
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'testpass'
|
||||||
|
}
|
||||||
|
service = new ErpAuthService(config)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have login method that returns a Promise', () => {
|
||||||
|
expect(service.login).toBeDefined()
|
||||||
|
expect(typeof service.login).toBe('function')
|
||||||
|
expect(service.login()).toBeInstanceOf(Promise)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have close method', () => {
|
||||||
|
expect(service.close).toBeDefined()
|
||||||
|
expect(typeof service.close).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have getSession method', () => {
|
||||||
|
expect(service.getSession).toBeDefined()
|
||||||
|
expect(typeof service.getSession).toBe('function')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have isActive method', () => {
|
||||||
|
expect(service.isActive).toBeDefined()
|
||||||
|
expect(typeof service.isActive).toBe('function')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user