Skip to content

Commit bc495c1

Browse files
authored
test(electron): Initial Electron SDK integration test (#9014)
1 parent cb76aa2 commit bc495c1

18 files changed

Lines changed: 431 additions & 1 deletion

File tree

.changeset/electron-e2e-tests.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

.github/workflows/ci.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ jobs:
347347
"custom",
348348
"hono",
349349
"chrome-extension",
350+
"electron",
350351
]
351352
test-project: ["chrome"]
352353
include:
@@ -486,10 +487,23 @@ jobs:
486487
working-directory: ./integration/certs
487488
run: ls -la && pwd
488489

490+
- name: Ensure Xvfb is installed
491+
if: ${{ matrix.test-name == 'electron' }}
492+
run: |
493+
if ! command -v xvfb-run &> /dev/null; then
494+
sudo apt-get update
495+
sudo apt-get install -y xvfb
496+
fi
497+
489498
- name: Run Integration Tests
490499
id: integration-tests
491500
timeout-minutes: 25
492-
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
501+
run: |
502+
if [ "${{ matrix.test-name }}" = "electron" ]; then
503+
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
504+
else
505+
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
506+
fi
493507
env:
494508
E2E_DEBUG: "1"
495509
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}

integration/presets/electron.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { applicationConfig } from '../models/applicationConfig';
2+
import { templates } from '../templates';
3+
import { PKGLAB } from './utils';
4+
5+
const vite = applicationConfig()
6+
.setName('electron-vite')
7+
.useTemplate(templates['electron-vite'])
8+
.setEnvFormatter('public', key => `VITE_${key}`)
9+
.addScript('setup', 'pnpm install')
10+
.addScript('dev', 'echo noop')
11+
.addScript('build', 'pnpm build')
12+
.addScript('serve', 'echo noop')
13+
.addDependency('@clerk/electron', PKGLAB);
14+
15+
export const electron = {
16+
vite,
17+
} as const;

integration/presets/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { astro } from './astro';
22
import { chromeExtension } from './chrome-extension';
33
import { customFlows } from './custom-flows';
4+
import { electron } from './electron';
45
import { envs, instanceKeys } from './envs';
56
import { expo } from './expo';
67
import { express } from './express';
@@ -17,6 +18,7 @@ import { vue } from './vue';
1718
export const appConfigs = {
1819
chromeExtension,
1920
customFlows,
21+
electron,
2022
envs,
2123
express,
2224
fastify,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1.0"
8+
/>
9+
<title>Clerk Electron E2E</title>
10+
</head>
11+
<body>
12+
<div id="root"></div>
13+
<script
14+
type="module"
15+
src="/src/main.tsx"
16+
></script>
17+
</body>
18+
</html>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import path from 'node:path';
2+
import { fileURLToPath, pathToFileURL } from 'node:url';
3+
4+
import { createClerkBridge } from '@clerk/electron';
5+
import { storage } from '@clerk/electron/storage';
6+
import { app, BrowserWindow, net, protocol } from 'electron';
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
9+
const RENDERER_SCHEME = 'clerk';
10+
const RENDERER_HOST = 'app';
11+
const rendererRoot = path.resolve(__dirname, 'dist');
12+
13+
const clerk = createClerkBridge({
14+
storage: storage({ unencryptedFallback: true }),
15+
renderer: {
16+
scheme: RENDERER_SCHEME,
17+
host: RENDERER_HOST,
18+
},
19+
});
20+
21+
async function createWindow() {
22+
const win = new BrowserWindow({
23+
width: 1000,
24+
height: 800,
25+
webPreferences: {
26+
contextIsolation: true,
27+
nodeIntegration: false,
28+
preload: path.resolve(__dirname, 'preload.mjs'),
29+
sandbox: false,
30+
},
31+
});
32+
33+
await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
34+
}
35+
36+
function registerClerkAppProtocol() {
37+
protocol.handle(RENDERER_SCHEME, async request => {
38+
const url = new URL(request.url);
39+
40+
if (url.host !== RENDERER_HOST) {
41+
return new Response('Not found', { status: 404 });
42+
}
43+
44+
let requestedPath;
45+
try {
46+
requestedPath = decodeURIComponent(url.pathname);
47+
} catch {
48+
return new Response('Bad request', { status: 400 });
49+
}
50+
51+
const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
52+
const relativePath = path.relative(rendererRoot, resolvedPath);
53+
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
54+
55+
if (!isSafe) {
56+
return new Response('Forbidden', { status: 403 });
57+
}
58+
59+
const hasExtension = /\.[^/]+$/.test(url.pathname);
60+
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');
61+
62+
return net.fetch(pathToFileURL(filePath).toString());
63+
});
64+
}
65+
66+
app.whenReady().then(async () => {
67+
registerClerkAppProtocol();
68+
await createWindow();
69+
});
70+
71+
app.on('window-all-closed', () => {
72+
app.quit();
73+
});
74+
75+
app.on('before-quit', () => {
76+
clerk.cleanup();
77+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "electron-vite",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "vite build"
8+
},
9+
"dependencies": {
10+
"electron-store": "^8.2.0",
11+
"react": "18.3.1",
12+
"react-dom": "18.3.1"
13+
},
14+
"devDependencies": {
15+
"@types/node": "^22.12.0",
16+
"@types/react": "18.3.12",
17+
"@types/react-dom": "18.3.1",
18+
"@vitejs/plugin-react": "^4.3.4",
19+
"electron": "^39.2.6",
20+
"typescript": "^5.7.3",
21+
"vite": "^7.3.3"
22+
},
23+
"engines": {
24+
"node": ">=24.15.0"
25+
}
26+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { exposeClerkBridge } from '@clerk/electron/preload';
2+
3+
exposeClerkBridge();
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
2+
import React from 'react';
3+
import ReactDOM from 'react-dom/client';
4+
5+
import './style.css';
6+
7+
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
8+
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
9+
10+
function App() {
11+
return (
12+
<ClerkProvider
13+
publishableKey={PUBLISHABLE_KEY}
14+
__internal_clerkUIUrl={CLERK_UI_URL}
15+
routerPush={() => {}}
16+
routerReplace={() => {}}
17+
>
18+
<main data-testid='electron-app'>
19+
<Show when='signed-out'>
20+
<SignIn />
21+
</Show>
22+
<Show when='signed-in'>
23+
<UserButton />
24+
<AuthInfo />
25+
</Show>
26+
</main>
27+
</ClerkProvider>
28+
);
29+
}
30+
31+
function AuthInfo() {
32+
const { sessionId, userId } = useAuth();
33+
34+
return (
35+
<div>
36+
<p data-testid='user-id'>{userId}</p>
37+
<p data-testid='session-id'>{sessionId}</p>
38+
</div>
39+
);
40+
}
41+
42+
ReactDOM.createRoot(document.getElementById('root')!).render(
43+
<React.StrictMode>
44+
<App />
45+
</React.StrictMode>,
46+
);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
body {
6+
min-width: 320px;
7+
min-height: 100vh;
8+
margin: 0;
9+
font-family:
10+
Inter,
11+
ui-sans-serif,
12+
system-ui,
13+
-apple-system,
14+
BlinkMacSystemFont,
15+
'Segoe UI',
16+
sans-serif;
17+
background: #f8fafc;
18+
}
19+
20+
main {
21+
display: grid;
22+
min-height: 100vh;
23+
place-items: center;
24+
padding: 24px;
25+
}

0 commit comments

Comments
 (0)