Skip to content

Commit 52b2b52

Browse files
authored
Add testing for electron app using windows github action runner (#5421)
1 parent 0d795c5 commit 52b2b52

File tree

10 files changed

+255
-85
lines changed

10 files changed

+255
-85
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Windows Electron Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
windows-electron-e2e:
13+
name: Windows Electron E2E Tests
14+
runs-on: windows-latest
15+
steps:
16+
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
17+
with:
18+
persist-credentials: false
19+
- uses: pnpm/action-setup@v4
20+
with:
21+
version: 10
22+
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
23+
with:
24+
node-version: '22'
25+
cache: 'pnpm'
26+
27+
- name: Install deps
28+
run: pnpm install --frozen-lockfile
29+
30+
- name: Build and test Electron on Windows
31+
run: pnpm test:e2e:ci:win
32+
working-directory: products/jbrowse-desktop
33+
env:
34+
NO_MINIMIZE: true

packages/core/src/util/tracks.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,24 @@ export function getFileName(track: FileLocation) {
181181
const uri = 'uri' in track ? track.uri : undefined
182182
const localPath = 'localPath' in track ? track.localPath : undefined
183183
const blob = 'blobId' in track ? track : undefined
184-
return (
185-
blob?.name ||
186-
uri?.slice(uri.lastIndexOf('/') + 1) ||
187-
localPath?.slice(localPath.replace(/\\/g, '/').lastIndexOf('/') + 1) ||
188-
''
189-
)
184+
185+
if (blob?.name) {
186+
return blob.name
187+
}
188+
189+
if (uri) {
190+
// Normalize path separators and find the last one
191+
// This handles both forward slashes and Windows backslashes in file:// URLs
192+
const normalized = uri.replace(/\\/g, '/')
193+
return normalized.slice(normalized.lastIndexOf('/') + 1)
194+
}
195+
196+
if (localPath) {
197+
const normalized = localPath.replace(/\\/g, '/')
198+
return normalized.slice(normalized.lastIndexOf('/') + 1)
199+
}
200+
201+
return ''
190202
}
191203

192204
export function guessAdapter(

packages/text-indexing/src/TextIndexing.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { checkStopToken } from '@jbrowse/core/util/stopToken'
77
import { ixIxxStream } from 'ixixx'
88

99
// misc
10-
import { generateMeta } from './types/common.ts'
10+
import { generateMeta, sanitizeForFilename } from './types/common.ts'
1111
import { indexGff3 } from './types/gff3Adapter.ts'
1212
import { indexVcf } from './types/vcfAdapter.ts'
1313

@@ -290,7 +290,8 @@ function getLoc(attr: string, config: Track) {
290290
}
291291

292292
function runIxIxx(readStream: Readable, idxLocation: string, name: string) {
293-
const ixFilename = path.join(idxLocation, 'trix', `${name}.ix`)
294-
const ixxFilename = path.join(idxLocation, 'trix', `${name}.ixx`)
293+
const safeName = sanitizeForFilename(name)
294+
const ixFilename = path.join(idxLocation, 'trix', `${safeName}.ix`)
295+
const ixxFilename = path.join(idxLocation, 'trix', `${safeName}.ixx`)
295296
return ixIxxStream(readStream, ixFilename, ixxFilename)
296297
}

packages/text-indexing/src/types/common.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import { fileURLToPath } from 'url'
34

45
import fetch from 'node-fetch'
56

@@ -31,12 +32,12 @@ export function isURL(FileName: string) {
3132
return url.protocol === 'http:' || url.protocol === 'https:'
3233
}
3334

34-
// Convert file:// URLs to local paths
35-
function fileUrlToPath(fileUrl: string): string | undefined {
35+
// Convert file:// URLs to local paths (handles Windows paths correctly)
36+
function convertFileUrlToPath(fileUrl: string): string | undefined {
3637
try {
3738
const url = new URL(fileUrl)
3839
if (url.protocol === 'file:') {
39-
return url.pathname
40+
return fileURLToPath(url)
4041
}
4142
} catch {
4243
// not a valid URL
@@ -66,7 +67,7 @@ export async function getLocalOrRemoteStream({
6667
return result.body
6768
} else {
6869
// Handle file:// URLs by converting to local path
69-
const localPath = fileUrlToPath(file) ?? file
70+
const localPath = convertFileUrlToPath(file) ?? file
7071
const filename = path.isAbsolute(localPath)
7172
? localPath
7273
: path.join(out, localPath)
@@ -152,6 +153,12 @@ export function guessAdapterFromFileName(filePath: string): Track {
152153
}
153154
}
154155

156+
// Sanitize a string to be safe for use in filenames on all platforms
157+
// Replaces characters that are invalid in Windows filenames: \ / : * ? " < > |
158+
export function sanitizeForFilename(name: string) {
159+
return name.replace(/[\\/:*?"<>|]/g, '_')
160+
}
161+
155162
/**
156163
* Generates metadata of index given a filename (trackId or assembly)
157164
*/
@@ -170,8 +177,9 @@ export async function generateMeta({
170177
featureTypesToExclude: string[]
171178
assemblyNames: string[]
172179
}) {
180+
const safeName = sanitizeForFilename(name)
173181
fs.writeFileSync(
174-
path.join(outDir, 'trix', `${name}_meta.json`),
182+
path.join(outDir, 'trix', `${safeName}_meta.json`),
175183
JSON.stringify(
176184
{
177185
dateCreated: new Date().toISOString(),

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

products/jbrowse-desktop/electron/autoUpdater.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,28 @@ export function setupAutoUpdater(
1919
autoUpdater.autoDownload = false
2020

2121
autoUpdater.on('error', (error: Error) => {
22+
const errorMessage = `Error in auto-updater: ${error}`
23+
sendStatusToWindow(getMainWindow(), errorMessage)
24+
25+
// Skip dialogs in CI environments to avoid blocking tests
26+
if (process.env.CI) {
27+
console.error('Auto-updater error (CI mode, skipping dialog):', error)
28+
return
29+
}
2230
dialog.showErrorBox(
23-
'Error: ',
31+
'Update Error',
2432
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2533
error == null ? 'unknown' : (error.stack || error).toString(),
2634
)
2735
})
2836

2937
autoUpdater.on('update-available', async () => {
38+
// Skip dialogs in CI environments
39+
if (process.env.CI) {
40+
// eslint-disable-next-line no-console
41+
console.log('Update available (CI mode, skipping dialog)')
42+
return
43+
}
3044
const result = await dialog.showMessageBox({
3145
type: 'info',
3246
title: 'Found updates',
@@ -46,11 +60,13 @@ export function setupAutoUpdater(
4660
sendStatusToWindow(getMainWindow(), 'Checking for update...')
4761
})
4862

49-
autoUpdater.on('error', (err: Error) => {
50-
sendStatusToWindow(getMainWindow(), `Error in auto-updater: ${err}`)
51-
})
52-
5363
autoUpdater.on('update-downloaded', () => {
64+
// Skip dialogs in CI environments
65+
if (process.env.CI) {
66+
// eslint-disable-next-line no-console
67+
console.log('Update downloaded (CI mode, skipping dialog)')
68+
return
69+
}
5470
dialog
5571
.showMessageBox({
5672
type: 'info',

products/jbrowse-desktop/electron/electron.ts

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, ipcMain } from 'electron'
1+
import { app, dialog, ipcMain } from 'electron'
22
import contextMenu from 'electron-context-menu'
33
import debug from 'electron-debug'
44
import pkg from 'electron-updater'
@@ -11,6 +11,7 @@ import { registerSessionHandlers } from './ipc/sessionHandlers.ts'
1111
import { initializePaths } from './paths.ts'
1212
import { createAuthWindow, createMainWindow } from './window.ts'
1313

14+
import type { AppPaths } from './paths.ts'
1415
import type { BrowserWindow } from 'electron'
1516

1617
const { autoUpdater } = pkg
@@ -22,31 +23,41 @@ debug({ showDevTools: false, isEnabled: true })
2223
// Environment variables
2324
const DEV_SERVER_URL = process.env.DEV_SERVER_URL
2425

25-
// Initialize paths once
26-
const paths = initializePaths()
27-
2826
// Main window reference
2927
let mainWindow: BrowserWindow | null = null
3028

29+
// Paths - initialized after app is ready
30+
let paths: AppPaths
31+
3132
function getMainWindow() {
3233
return mainWindow
3334
}
3435

36+
/**
37+
* Shows an error dialog to the user and optionally quits the app
38+
*/
39+
function showFatalError(title: string, error: unknown, shouldQuit = true) {
40+
const message = error instanceof Error ? error.message : String(error)
41+
const detail = error instanceof Error ? error.stack : undefined
42+
console.error(`${title}:`, error)
43+
44+
dialog.showErrorBox(title, detail ? `${message}\n\n${detail}` : message)
45+
46+
if (shouldQuit) {
47+
app.quit()
48+
}
49+
}
50+
3551
/**
3652
* Creates the main window and initializes the application
3753
*/
3854
async function initialize() {
39-
try {
40-
await initializeFileSystem(paths)
41-
mainWindow = await createMainWindow(autoUpdater, DEV_SERVER_URL)
55+
await initializeFileSystem(paths)
56+
mainWindow = await createMainWindow(autoUpdater, DEV_SERVER_URL)
4257

43-
mainWindow.on('closed', () => {
44-
mainWindow = null
45-
})
46-
} catch (error) {
47-
console.error('Failed to initialize application:', error)
48-
throw error
49-
}
58+
mainWindow.on('closed', () => {
59+
mainWindow = null
60+
})
5061
}
5162

5263
/**
@@ -72,15 +83,19 @@ function registerIpcHandlers() {
7283
)
7384
}
7485

75-
// Setup auto-updater
86+
// Setup auto-updater (just registers event listeners, safe before ready)
7687
setupAutoUpdater(autoUpdater, getMainWindow)
7788

78-
// Register IPC handlers
79-
registerIpcHandlers()
80-
8189
// App lifecycle handlers
8290
app.on('ready', async () => {
83-
await initialize()
91+
try {
92+
// Initialize paths after app is ready to ensure app.getPath() works reliably
93+
paths = initializePaths()
94+
registerIpcHandlers()
95+
await initialize()
96+
} catch (error) {
97+
showFatalError('Failed to initialize application', error)
98+
}
8499
})
85100

86101
app.on('window-all-closed', () => {
@@ -91,6 +106,10 @@ app.on('window-all-closed', () => {
91106

92107
app.on('activate', async () => {
93108
if (mainWindow === null) {
94-
await initialize()
109+
try {
110+
await initialize()
111+
} catch (error) {
112+
showFatalError('Failed to create window', error)
113+
}
95114
}
96115
})

products/jbrowse-desktop/electron/window.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'path'
2-
import url from 'url'
2+
import url, { pathToFileURL } from 'url'
33

44
import electron, { BrowserWindow, Menu, app, shell } from 'electron'
55
import windowStateKeeper from 'electron-window-state'
@@ -15,9 +15,7 @@ const DEFAULT_DEV_SERVER_URL = 'http://localhost:3000'
1515

1616
function getAppUrl(devServerUrl: URL): URL {
1717
if (app.isPackaged) {
18-
return new URL(
19-
`file://${path.join(app.getAppPath(), 'build', 'index.html')}`,
20-
)
18+
return pathToFileURL(path.join(app.getAppPath(), 'build', 'index.html'))
2119
}
2220
return devServerUrl
2321
}
@@ -84,11 +82,14 @@ export async function createMainWindow(
8482
mainWindowState.manage(mainWindow)
8583

8684
// This ready-to-show handler must be attached before the loadURL
87-
mainWindow.once('ready-to-show', () => {
88-
autoUpdater.checkForUpdatesAndNotify().catch((e: unknown) => {
89-
console.error(e)
85+
// Skip auto-update check in CI environments to avoid blocking dialogs
86+
if (!process.env.CI) {
87+
mainWindow.once('ready-to-show', () => {
88+
autoUpdater.checkForUpdatesAndNotify().catch((e: unknown) => {
89+
console.error(e)
90+
})
9091
})
91-
})
92+
}
9293

9394
const serverUrl = new URL(devServerUrl || DEFAULT_DEV_SERVER_URL)
9495
const appUrl = getAppUrl(serverUrl)

0 commit comments

Comments
 (0)