feat: integrate Playwright download dialog into startup flow
This commit is contained in:
@@ -31,7 +31,11 @@ export function setupElectronRuntime(): void {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensurePlaywrightRuntime(browsersPath: string): void {
|
/**
|
||||||
|
* Check if Playwright browsers are installed
|
||||||
|
* @returns true if browsers exist, false otherwise
|
||||||
|
*/
|
||||||
|
export function ensurePlaywrightRuntime(browsersPath: string): boolean {
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(browsersPath, { recursive: true })
|
fs.mkdirSync(browsersPath, { recursive: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -43,7 +47,7 @@ export function ensurePlaywrightRuntime(browsersPath: string): void {
|
|||||||
const chromiumPath = fs.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath
|
const chromiumPath = fs.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath
|
||||||
|
|
||||||
if (fs.existsSync(chromiumPath)) {
|
if (fs.existsSync(chromiumPath)) {
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
let foundRevision = false
|
let foundRevision = false
|
||||||
@@ -64,22 +68,14 @@ export function ensurePlaywrightRuntime(browsersPath: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (foundRevision) {
|
if (foundRevision) {
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.showErrorBox(
|
|
||||||
'浏览器文件未找到',
|
|
||||||
`Playwright 浏览器文件不存在。\n\n` +
|
|
||||||
`期望路径:${newChromiumPath}\n` +
|
|
||||||
`或:${oldChromiumPath}\n\n` +
|
|
||||||
`当前目录内容:${fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath).join(', ') : '目录不存在'}\n\n` +
|
|
||||||
`请运行以下命令安装浏览器:\n` +
|
|
||||||
`npx playwright install chromium`
|
|
||||||
)
|
|
||||||
console.warn(
|
console.warn(
|
||||||
'Playwright browser not found. Available:',
|
'Playwright browser not found. Available:',
|
||||||
fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none'
|
fs.existsSync(browsersPath) ? fs.readdirSync(browsersPath) : 'none'
|
||||||
)
|
)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeMainProcessServices(): Promise<void> {
|
export async function initializeMainProcessServices(): Promise<void> {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ app.whenReady().then(async () => {
|
|||||||
setupProcessGuards()
|
setupProcessGuards()
|
||||||
registerMainWindowLifecycle()
|
registerMainWindowLifecycle()
|
||||||
const playwrightBrowsersPath = configurePlaywrightBrowsersPath()
|
const playwrightBrowsersPath = configurePlaywrightBrowsersPath()
|
||||||
ensurePlaywrightRuntime(playwrightBrowsersPath)
|
const browsersExist = ensurePlaywrightRuntime(playwrightBrowsersPath)
|
||||||
|
console.log('Playwright browsers exist:', browsersExist)
|
||||||
await initializeMainProcessServices()
|
await initializeMainProcessServices()
|
||||||
setupElectronRuntime()
|
setupElectronRuntime()
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
* Handles browser download requests from renderer process
|
* Handles browser download requests from renderer process
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ipcMain, IpcMainInvokeEvent } from 'electron'
|
import { app, ipcMain, IpcMainInvokeEvent } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||||
import { withErrorHandling, type IpcResult } from './index'
|
import { withErrorHandling, type IpcResult } from './index'
|
||||||
import { DownloadService } from '../services/playwright-browser'
|
import { DownloadService } from '../services/playwright-browser'
|
||||||
@@ -31,12 +32,51 @@ function createS3Client(): S3Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Playwright browsers are installed
|
||||||
|
*/
|
||||||
|
async function checkBrowsersExist(): Promise<boolean> {
|
||||||
|
const fs = await import('fs')
|
||||||
|
const browsersPath = join(app.getPath('userData'), 'ms-playwright')
|
||||||
|
const newChromiumPath = join(browsersPath, 'chromium-1208', 'chrome-win64', 'chrome.exe')
|
||||||
|
const oldChromiumPath = join(browsersPath, 'chromium-win32', 'chrome.exe')
|
||||||
|
const chromiumPath = fs.default.existsSync(newChromiumPath) ? newChromiumPath : oldChromiumPath
|
||||||
|
|
||||||
|
if (fs.default.existsSync(chromiumPath)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundRevision = false
|
||||||
|
try {
|
||||||
|
const entries = fs.default.readdirSync(browsersPath)
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.startsWith('chromium-') && !entry.includes('headless')) {
|
||||||
|
const revisionPath = join(browsersPath, entry, 'chrome-win64', 'chrome.exe')
|
||||||
|
if (fs.default.existsSync(revisionPath)) {
|
||||||
|
foundRevision = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore browser directory probing failures
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundRevision
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track active download for cancellation
|
* Track active download for cancellation
|
||||||
*/
|
*/
|
||||||
let activeDownload: { service: DownloadService; cancelled: boolean } | null = null
|
let activeDownload: { service: DownloadService; cancelled: boolean } | null = null
|
||||||
|
|
||||||
export function registerPlaywrightBrowserHandlers(): void {
|
export function registerPlaywrightBrowserHandlers(): void {
|
||||||
|
ipcMain.handle(IPC_CHANNELS.PLAYWRIGHT_BROWSER_CHECK, async (): Promise<IpcResult<boolean>> => {
|
||||||
|
return withErrorHandling(async () => {
|
||||||
|
return checkBrowsersExist()
|
||||||
|
}, 'playwright-browser:check')
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD,
|
IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD,
|
||||||
async (event: IpcMainInvokeEvent): Promise<IpcResult<void>> => {
|
async (event: IpcMainInvokeEvent): Promise<IpcResult<void>> => {
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import type { IpcResult } from '../../main/types/ipc.types'
|
|||||||
import type { DownloadProgress } from '../index.d'
|
import type { DownloadProgress } from '../index.d'
|
||||||
|
|
||||||
export const playwrightBrowserApi = {
|
export const playwrightBrowserApi = {
|
||||||
|
check: async (): Promise<IpcResult<boolean>> => {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.PLAYWRIGHT_BROWSER_CHECK)
|
||||||
|
},
|
||||||
|
|
||||||
download: async (): Promise<IpcResult<void>> => {
|
download: async (): Promise<IpcResult<void>> => {
|
||||||
return ipcRenderer.invoke(IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD)
|
return ipcRenderer.invoke(IPC_CHANNELS.PLAYWRIGHT_BROWSER_DOWNLOAD)
|
||||||
},
|
},
|
||||||
|
|||||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@@ -151,6 +151,7 @@ export interface DownloadProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PlaywrightBrowserAPI {
|
export interface PlaywrightBrowserAPI {
|
||||||
|
check: () => Promise<IpcResult<boolean>>
|
||||||
download: () => Promise<IpcResult<void>>
|
download: () => Promise<IpcResult<void>>
|
||||||
cancel: () => Promise<IpcResult<void>>
|
cancel: () => Promise<IpcResult<void>>
|
||||||
onProgress: (callback: (data: DownloadProgress) => void) => () => void
|
onProgress: (callback: (data: DownloadProgress) => void) => () => void
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { AuthenticatedAppShell } from './components/app/AuthenticatedAppShell'
|
import { AuthenticatedAppShell } from './components/app/AuthenticatedAppShell'
|
||||||
import { UnauthenticatedApp } from './components/app/UnauthenticatedApp'
|
import { UnauthenticatedApp } from './components/app/UnauthenticatedApp'
|
||||||
|
import PlaywrightDownloadDialog from './components/PlaywrightDownloadDialog'
|
||||||
import { useAppBootstrap } from './hooks/useAppBootstrap'
|
import { useAppBootstrap } from './hooks/useAppBootstrap'
|
||||||
|
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
@@ -22,6 +23,8 @@ function App(): React.JSX.Element {
|
|||||||
updateCatalog,
|
updateCatalog,
|
||||||
showUpdateDialog,
|
showUpdateDialog,
|
||||||
setShowUpdateDialog,
|
setShowUpdateDialog,
|
||||||
|
showPlaywrightDownload,
|
||||||
|
setShowPlaywrightDownload,
|
||||||
showError,
|
showError,
|
||||||
handleLogin,
|
handleLogin,
|
||||||
handleLoginCancel,
|
handleLoginCancel,
|
||||||
@@ -34,8 +37,23 @@ function App(): React.JSX.Element {
|
|||||||
refreshUpdateDialogState
|
refreshUpdateDialogState
|
||||||
} = useAppBootstrap()
|
} = useAppBootstrap()
|
||||||
|
|
||||||
|
const handlePlaywrightDownloadComplete = React.useCallback(() => {
|
||||||
|
setShowPlaywrightDownload(false)
|
||||||
|
}, [setShowPlaywrightDownload])
|
||||||
|
|
||||||
const shouldShowLogout = currentUser?.userType === 'Admin' || isSwitchedByAdmin
|
const shouldShowLogout = currentUser?.userType === 'Admin' || isSwitchedByAdmin
|
||||||
|
|
||||||
|
// Show Playwright download dialog first (before authentication check)
|
||||||
|
if (showPlaywrightDownload) {
|
||||||
|
return (
|
||||||
|
<PlaywrightDownloadDialog
|
||||||
|
isOpen={showPlaywrightDownload}
|
||||||
|
onClose={() => {}}
|
||||||
|
onDownloadComplete={handlePlaywrightDownloadComplete}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<UnauthenticatedApp
|
<UnauthenticatedApp
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function useAppBootstrap() {
|
|||||||
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
|
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
|
||||||
const [updateCatalog, setUpdateCatalog] = useState<UpdateDialogCatalog | null>(null)
|
const [updateCatalog, setUpdateCatalog] = useState<UpdateDialogCatalog | null>(null)
|
||||||
const [showUpdateDialog, setShowUpdateDialog] = useState(false)
|
const [showUpdateDialog, setShowUpdateDialog] = useState(false)
|
||||||
|
const [showPlaywrightDownload, setShowPlaywrightDownload] = useState(false)
|
||||||
|
|
||||||
const authInitializationStartedRef = useRef(false)
|
const authInitializationStartedRef = useRef(false)
|
||||||
|
|
||||||
@@ -120,7 +121,28 @@ export function useAppBootstrap() {
|
|||||||
|
|
||||||
authInitializationStartedRef.current = true
|
authInitializationStartedRef.current = true
|
||||||
logger.info('=== Initializing auth... ===')
|
logger.info('=== Initializing auth... ===')
|
||||||
|
|
||||||
|
// Check if Playwright browsers are installed
|
||||||
|
const checkPlaywrightBrowsers = async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.electron.playwrightBrowser.check()
|
||||||
|
if (result.success && !result.data) {
|
||||||
|
logger.info('Playwright browsers not found, showing download dialog')
|
||||||
|
setShowPlaywrightDownload(true)
|
||||||
|
} else {
|
||||||
|
logger.info('Playwright browsers found, continuing auth')
|
||||||
void initializeAuth()
|
void initializeAuth()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to check Playwright browsers', {
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
})
|
||||||
|
// Continue with auth even if check fails
|
||||||
|
void initializeAuth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkPlaywrightBrowsers()
|
||||||
}, [initializeAuth, logger])
|
}, [initializeAuth, logger])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -295,6 +317,8 @@ export function useAppBootstrap() {
|
|||||||
updateCatalog,
|
updateCatalog,
|
||||||
showUpdateDialog,
|
showUpdateDialog,
|
||||||
setShowUpdateDialog,
|
setShowUpdateDialog,
|
||||||
|
showPlaywrightDownload,
|
||||||
|
setShowPlaywrightDownload,
|
||||||
showError,
|
showError,
|
||||||
refreshUpdateState,
|
refreshUpdateState,
|
||||||
refreshUpdateCatalog,
|
refreshUpdateCatalog,
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ export const IPC_CHANNELS = {
|
|||||||
// Playwright Browser
|
// Playwright Browser
|
||||||
PLAYWRIGHT_BROWSER_DOWNLOAD: 'playwright-browser:download',
|
PLAYWRIGHT_BROWSER_DOWNLOAD: 'playwright-browser:download',
|
||||||
PLAYWRIGHT_BROWSER_CANCEL: 'playwright-browser:cancel',
|
PLAYWRIGHT_BROWSER_CANCEL: 'playwright-browser:cancel',
|
||||||
PLAYWRIGHT_BROWSER_PROGRESS: 'playwright-browser:progress'
|
PLAYWRIGHT_BROWSER_PROGRESS: 'playwright-browser:progress',
|
||||||
|
PLAYWRIGHT_BROWSER_CHECK: 'playwright-browser:check'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user