Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/electron-e2e-tests.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,7 @@ jobs:
"custom",
"hono",
"chrome-extension",
"electron",
]
test-project: ["chrome"]
include:
Expand DownExpand Up@@ -486,10 +487,23 @@ jobs:
working-directory: ./integration/certs
run: ls -la && pwd

- name: Ensure Xvfb is installed
if: ${{ matrix.test-name == 'electron' }}
run: |
if ! command -v xvfb-run &> /dev/null; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
run: |
if [ "${{ matrix.test-name }}" = "electron" ]; then
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
else
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
E2E_DEBUG: "1"
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
Expand Down
17 changes: 17 additions & 0 deletions integration/presets/electron.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { PKGLAB } from './utils';

const vite = applicationConfig()
.setName('electron-vite')
.useTemplate(templates['electron-vite'])
.setEnvFormatter('public', key => `VITE_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'echo noop')
.addScript('build', 'pnpm build')
.addScript('serve', 'echo noop')
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire dev and serve to real commands.

pnpm dev currently just performs a build, and pnpm serve exits immediately, so the generated Electron template has no runnable development or start path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/presets/electron.ts` around lines 9 - 12, The Electron preset
currently wires the package scripts in `integration/presets/electron.ts` so
`dev` only builds and `serve` is a noop, which leaves no runnable app flow.
Update the `addScript('dev', ...)` and `addScript('serve', ...)` entries to
point to the actual Electron development and startup commands used by the
template, keeping `setup` and `build` unchanged. Use the existing script wiring
in the preset as the place to fix this so generated projects expose a real `dev`
path and a real `serve` path.

.addDependency('@clerk/electron', PKGLAB);

export const electron = {
vite,
} as const;
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { astro } from './astro';
import { chromeExtension } from './chrome-extension';
import { customFlows } from './custom-flows';
import { electron } from './electron';
import { envs, instanceKeys } from './envs';
import { expo } from './expo';
import { express } from './express';
Expand All@@ -17,6 +18,7 @@ import { vue } from './vue';
export const appConfigs = {
chromeExtension,
customFlows,
electron,
envs,
express,
fastify,
Expand Down
18 changes: 18 additions & 0 deletions integration/templates/electron-vite/index.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Clerk Electron E2E</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
77 changes: 77 additions & 0 deletions integration/templates/electron-vite/main.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { createClerkBridge } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';
import { app, BrowserWindow, net, protocol } from 'electron';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RENDERER_SCHEME = 'clerk';
const RENDERER_HOST = 'app';
const rendererRoot = path.resolve(__dirname, 'dist');

const clerk = createClerkBridge({
storage: storage({ unencryptedFallback: true }),
renderer: {
scheme: RENDERER_SCHEME,
host: RENDERER_HOST,
},
});

async function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.mjs'),
sandbox: false,
},
});

await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
}

function registerClerkAppProtocol() {
protocol.handle(RENDERER_SCHEME, async request => {
const url = new URL(request.url);

if (url.host !== RENDERER_HOST) {
return new Response('Not found', { status: 404 });
}

let requestedPath;
try {
requestedPath = decodeURIComponent(url.pathname);
} catch {
return new Response('Bad request', { status: 400 });
}

const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
const relativePath = path.relative(rendererRoot, resolvedPath);
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));

if (!isSafe) {
return new Response('Forbidden', { status: 403 });
}

const hasExtension = /\.[^/]+$/.test(url.pathname);
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');

return net.fetch(pathToFileURL(filePath).toString());
});
}

app.whenReady().then(async () => {
registerClerkAppProtocol();
await createWindow();
});

app.on('window-all-closed', () => {
app.quit();
});

app.on('before-quit', () => {
clerk.cleanup();
});
26 changes: 26 additions & 0 deletions integration/templates/electron-vite/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
{
"name": "electron-vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"dependencies": {
"electron-store": "^8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^22.12.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.6",
"typescript": "^5.7.3",
"vite": "^7.3.3"
},
"engines": {
"node": ">=24.15.0"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions integration/templates/electron-vite/preload.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { exposeClerkBridge } from '@clerk/electron/preload';

exposeClerkBridge();
46 changes: 46 additions & 0 deletions integration/templates/electron-vite/src/main.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
import React from 'react';
import ReactDOM from 'react-dom/client';

import './style.css';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the required Vite env vars at startup.

The as string assertions only silence TypeScript; an unset VITE_CLERK_* still flows through as undefined and fails later inside Clerk initialization. Throw a clear startup error here instead.

🛠️ Proposed change
-const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;-const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;+const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL;++if (!PUBLISHABLE_KEY || !CLERK_UI_URL) {+ throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');+}

As per coding guidelines, "Validate all inputs and sanitize outputs" and "Provide meaningful error messages to developers".

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEYasstring;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URLasstring;
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URL;
if(!PUBLISHABLE_KEY||!CLERK_UI_URL){
thrownewError('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/templates/electron-vite/src/main.tsx` around lines 7 - 8, The
startup constants in main.tsx rely on TypeScript assertions only, so missing
VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL can still reach Clerk as
undefined. Update the initialization around PUBLISHABLE_KEY and CLERK_UI_URL to
validate both env vars immediately at startup and throw a clear,
developer-facing error if either is absent or empty. Keep the check close to the
existing env reads so the failure happens before Clerk setup, and make the
message specific enough to identify which required variable is missing.

Source: Coding guidelines


function App() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
__internal_clerkUIUrl={CLERK_UI_URL}
routerPush={() => {}}
routerReplace={() => {}}
>
<main data-testid='electron-app'>
<Show when='signed-out'>
<SignIn />
</Show>
<Show when='signed-in'>
<UserButton />
<AuthInfo />
</Show>
</main>
</ClerkProvider>
);
}

function AuthInfo() {
const { sessionId, userId } = useAuth();

return (
<div>
<p data-testid='user-id'>{userId}</p>
<p data-testid='session-id'>{sessionId}</p>
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
25 changes: 25 additions & 0 deletions integration/templates/electron-vite/src/style.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
background: #f8fafc;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
}
1 change: 1 addition & 0 deletions integration/templates/electron-vite/src/vite-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions integration/templates/electron-vite/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions integration/templates/electron-vite/vite.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const templates = {
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'chrome-extension-vite': resolve(__dirname, './chrome-extension-vite'),
'electron-vite': resolve(__dirname, './electron-vite'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
77 changes: 77 additions & 0 deletions integration/tests/electron/basic.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import { clerk } from '@clerk/testing/playwright';
import { createPageObjects } from '@clerk/testing/playwright/unstable';

import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { expect, test } from './fixtures';

type ElectronWindow = Window & {
__clerk_internal_electron?: {
tokenCache?: Partial<Record<'clearToken' | 'getToken' | 'saveToken', unknown>>;
oauthTransport?: Partial<Record<'getRedirectUrl' | 'open', unknown>>;
};
};

test.describe('electron basic auth @electron', () => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async ({ electronTestApp }) => {
const u = createTestUtils({ app: electronTestApp });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser?.deleteIfExists();
});

test('exposes the preload bridge to the renderer', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="electron-app"]')).toBeVisible();

await expect(
electronPage.evaluate(() => {
const bridge = (window as ElectronWindow).__clerk_internal_electron;

return (
typeof bridge?.tokenCache?.clearToken === 'function' &&
typeof bridge?.tokenCache?.getToken === 'function' &&
typeof bridge?.tokenCache?.saveToken === 'function' &&
typeof bridge?.oauthTransport?.getRedirectUrl === 'function' &&
typeof bridge?.oauthTransport?.open === 'function'
);
}),
).resolves.toBe(true);
});

test('signs in with email and password', async ({ electronPage }) => {
const { signIn } = createPageObjects({ page: electronPage, useTestingToken: false });

await signIn.waitForMounted();
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible();

await signIn.setIdentifier(fakeUser.email!);
await signIn.continue();
await signIn.setPassword(fakeUser.password);
await signIn.continue();

await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
});

test('persists the signed-in session after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
await expect(electronPage.locator('[data-testid="session-id"]')).toHaveText(/^sess_/, { timeout: 30_000 });
});

test('signs out and clears the session', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-userButtonTrigger')).toBeVisible({ timeout: 30_000 });
await clerk.signOut({ page: electronPage });

await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});

test('keeps the signed-out state after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/electron-e2e-tests.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,7 @@ jobs:
"custom",
"hono",
"chrome-extension",
"electron",
]
test-project: ["chrome"]
include:
Expand DownExpand Up@@ -486,10 +487,23 @@ jobs:
working-directory: ./integration/certs
run: ls -la && pwd

- name: Ensure Xvfb is installed
if: ${{ matrix.test-name == 'electron' }}
run: |
if ! command -v xvfb-run &> /dev/null; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
run: |
if [ "${{ matrix.test-name }}" = "electron" ]; then
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
else
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
E2E_DEBUG: "1"
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
Expand Down
17 changes: 17 additions & 0 deletions integration/presets/electron.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { PKGLAB } from './utils';

const vite = applicationConfig()
.setName('electron-vite')
.useTemplate(templates['electron-vite'])
.setEnvFormatter('public', key => `VITE_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'echo noop')
.addScript('build', 'pnpm build')
.addScript('serve', 'echo noop')
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire dev and serve to real commands.

pnpm dev currently just performs a build, and pnpm serve exits immediately, so the generated Electron template has no runnable development or start path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/presets/electron.ts` around lines 9 - 12, The Electron preset
currently wires the package scripts in `integration/presets/electron.ts` so
`dev` only builds and `serve` is a noop, which leaves no runnable app flow.
Update the `addScript('dev', ...)` and `addScript('serve', ...)` entries to
point to the actual Electron development and startup commands used by the
template, keeping `setup` and `build` unchanged. Use the existing script wiring
in the preset as the place to fix this so generated projects expose a real `dev`
path and a real `serve` path.

.addDependency('@clerk/electron', PKGLAB);

export const electron = {
vite,
} as const;
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { astro } from './astro';
import { chromeExtension } from './chrome-extension';
import { customFlows } from './custom-flows';
import { electron } from './electron';
import { envs, instanceKeys } from './envs';
import { expo } from './expo';
import { express } from './express';
Expand All@@ -17,6 +18,7 @@ import { vue } from './vue';
export const appConfigs = {
chromeExtension,
customFlows,
electron,
envs,
express,
fastify,
Expand Down
18 changes: 18 additions & 0 deletions integration/templates/electron-vite/index.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Clerk Electron E2E</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
77 changes: 77 additions & 0 deletions integration/templates/electron-vite/main.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { createClerkBridge } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';
import { app, BrowserWindow, net, protocol } from 'electron';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RENDERER_SCHEME = 'clerk';
const RENDERER_HOST = 'app';
const rendererRoot = path.resolve(__dirname, 'dist');

const clerk = createClerkBridge({
storage: storage({ unencryptedFallback: true }),
renderer: {
scheme: RENDERER_SCHEME,
host: RENDERER_HOST,
},
});

async function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.mjs'),
sandbox: false,
},
});

await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
}

function registerClerkAppProtocol() {
protocol.handle(RENDERER_SCHEME, async request => {
const url = new URL(request.url);

if (url.host !== RENDERER_HOST) {
return new Response('Not found', { status: 404 });
}

let requestedPath;
try {
requestedPath = decodeURIComponent(url.pathname);
} catch {
return new Response('Bad request', { status: 400 });
}

const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
const relativePath = path.relative(rendererRoot, resolvedPath);
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));

if (!isSafe) {
return new Response('Forbidden', { status: 403 });
}

const hasExtension = /\.[^/]+$/.test(url.pathname);
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');

return net.fetch(pathToFileURL(filePath).toString());
});
}

app.whenReady().then(async () => {
registerClerkAppProtocol();
await createWindow();
});

app.on('window-all-closed', () => {
app.quit();
});

app.on('before-quit', () => {
clerk.cleanup();
});
26 changes: 26 additions & 0 deletions integration/templates/electron-vite/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
{
"name": "electron-vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"dependencies": {
"electron-store": "^8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^22.12.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.6",
"typescript": "^5.7.3",
"vite": "^7.3.3"
},
"engines": {
"node": ">=24.15.0"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions integration/templates/electron-vite/preload.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { exposeClerkBridge } from '@clerk/electron/preload';

exposeClerkBridge();
46 changes: 46 additions & 0 deletions integration/templates/electron-vite/src/main.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
import React from 'react';
import ReactDOM from 'react-dom/client';

import './style.css';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the required Vite env vars at startup.

The as string assertions only silence TypeScript; an unset VITE_CLERK_* still flows through as undefined and fails later inside Clerk initialization. Throw a clear startup error here instead.

🛠️ Proposed change
-const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;-const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;+const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL;++if (!PUBLISHABLE_KEY || !CLERK_UI_URL) {+ throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');+}

As per coding guidelines, "Validate all inputs and sanitize outputs" and "Provide meaningful error messages to developers".

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEYasstring;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URLasstring;
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URL;
if(!PUBLISHABLE_KEY||!CLERK_UI_URL){
thrownewError('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/templates/electron-vite/src/main.tsx` around lines 7 - 8, The
startup constants in main.tsx rely on TypeScript assertions only, so missing
VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL can still reach Clerk as
undefined. Update the initialization around PUBLISHABLE_KEY and CLERK_UI_URL to
validate both env vars immediately at startup and throw a clear,
developer-facing error if either is absent or empty. Keep the check close to the
existing env reads so the failure happens before Clerk setup, and make the
message specific enough to identify which required variable is missing.

Source: Coding guidelines


function App() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
__internal_clerkUIUrl={CLERK_UI_URL}
routerPush={() => {}}
routerReplace={() => {}}
>
<main data-testid='electron-app'>
<Show when='signed-out'>
<SignIn />
</Show>
<Show when='signed-in'>
<UserButton />
<AuthInfo />
</Show>
</main>
</ClerkProvider>
);
}

function AuthInfo() {
const { sessionId, userId } = useAuth();

return (
<div>
<p data-testid='user-id'>{userId}</p>
<p data-testid='session-id'>{sessionId}</p>
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
25 changes: 25 additions & 0 deletions integration/templates/electron-vite/src/style.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
background: #f8fafc;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
}
1 change: 1 addition & 0 deletions integration/templates/electron-vite/src/vite-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions integration/templates/electron-vite/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions integration/templates/electron-vite/vite.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const templates = {
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'chrome-extension-vite': resolve(__dirname, './chrome-extension-vite'),
'electron-vite': resolve(__dirname, './electron-vite'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
77 changes: 77 additions & 0 deletions integration/tests/electron/basic.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import { clerk } from '@clerk/testing/playwright';
import { createPageObjects } from '@clerk/testing/playwright/unstable';

import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { expect, test } from './fixtures';

type ElectronWindow = Window & {
__clerk_internal_electron?: {
tokenCache?: Partial<Record<'clearToken' | 'getToken' | 'saveToken', unknown>>;
oauthTransport?: Partial<Record<'getRedirectUrl' | 'open', unknown>>;
};
};

test.describe('electron basic auth @electron', () => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async ({ electronTestApp }) => {
const u = createTestUtils({ app: electronTestApp });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser?.deleteIfExists();
});

test('exposes the preload bridge to the renderer', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="electron-app"]')).toBeVisible();

await expect(
electronPage.evaluate(() => {
const bridge = (window as ElectronWindow).__clerk_internal_electron;

return (
typeof bridge?.tokenCache?.clearToken === 'function' &&
typeof bridge?.tokenCache?.getToken === 'function' &&
typeof bridge?.tokenCache?.saveToken === 'function' &&
typeof bridge?.oauthTransport?.getRedirectUrl === 'function' &&
typeof bridge?.oauthTransport?.open === 'function'
);
}),
).resolves.toBe(true);
});

test('signs in with email and password', async ({ electronPage }) => {
const { signIn } = createPageObjects({ page: electronPage, useTestingToken: false });

await signIn.waitForMounted();
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible();

await signIn.setIdentifier(fakeUser.email!);
await signIn.continue();
await signIn.setPassword(fakeUser.password);
await signIn.continue();

await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
});

test('persists the signed-in session after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
await expect(electronPage.locator('[data-testid="session-id"]')).toHaveText(/^sess_/, { timeout: 30_000 });
});

test('signs out and clears the session', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-userButtonTrigger')).toBeVisible({ timeout: 30_000 });
await clerk.signOut({ page: electronPage });

await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});

test('keeps the signed-out state after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/electron-e2e-tests.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,7 @@ jobs:
"custom",
"hono",
"chrome-extension",
"electron",
]
test-project: ["chrome"]
include:
Expand DownExpand Up@@ -486,10 +487,23 @@ jobs:
working-directory: ./integration/certs
run: ls -la && pwd

- name: Ensure Xvfb is installed
if: ${{ matrix.test-name == 'electron' }}
run: |
if ! command -v xvfb-run &> /dev/null; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
run: |
if [ "${{ matrix.test-name }}" = "electron" ]; then
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
else
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
E2E_DEBUG: "1"
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
Expand Down
17 changes: 17 additions & 0 deletions integration/presets/electron.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { PKGLAB } from './utils';

const vite = applicationConfig()
.setName('electron-vite')
.useTemplate(templates['electron-vite'])
.setEnvFormatter('public', key => `VITE_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'echo noop')
.addScript('build', 'pnpm build')
.addScript('serve', 'echo noop')
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire dev and serve to real commands.

pnpm dev currently just performs a build, and pnpm serve exits immediately, so the generated Electron template has no runnable development or start path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/presets/electron.ts` around lines 9 - 12, The Electron preset
currently wires the package scripts in `integration/presets/electron.ts` so
`dev` only builds and `serve` is a noop, which leaves no runnable app flow.
Update the `addScript('dev', ...)` and `addScript('serve', ...)` entries to
point to the actual Electron development and startup commands used by the
template, keeping `setup` and `build` unchanged. Use the existing script wiring
in the preset as the place to fix this so generated projects expose a real `dev`
path and a real `serve` path.

.addDependency('@clerk/electron', PKGLAB);

export const electron = {
vite,
} as const;
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { astro } from './astro';
import { chromeExtension } from './chrome-extension';
import { customFlows } from './custom-flows';
import { electron } from './electron';
import { envs, instanceKeys } from './envs';
import { expo } from './expo';
import { express } from './express';
Expand All@@ -17,6 +18,7 @@ import { vue } from './vue';
export const appConfigs = {
chromeExtension,
customFlows,
electron,
envs,
express,
fastify,
Expand Down
18 changes: 18 additions & 0 deletions integration/templates/electron-vite/index.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Clerk Electron E2E</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
77 changes: 77 additions & 0 deletions integration/templates/electron-vite/main.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { createClerkBridge } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';
import { app, BrowserWindow, net, protocol } from 'electron';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RENDERER_SCHEME = 'clerk';
const RENDERER_HOST = 'app';
const rendererRoot = path.resolve(__dirname, 'dist');

const clerk = createClerkBridge({
storage: storage({ unencryptedFallback: true }),
renderer: {
scheme: RENDERER_SCHEME,
host: RENDERER_HOST,
},
});

async function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.mjs'),
sandbox: false,
},
});

await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
}

function registerClerkAppProtocol() {
protocol.handle(RENDERER_SCHEME, async request => {
const url = new URL(request.url);

if (url.host !== RENDERER_HOST) {
return new Response('Not found', { status: 404 });
}

let requestedPath;
try {
requestedPath = decodeURIComponent(url.pathname);
} catch {
return new Response('Bad request', { status: 400 });
}

const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
const relativePath = path.relative(rendererRoot, resolvedPath);
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));

if (!isSafe) {
return new Response('Forbidden', { status: 403 });
}

const hasExtension = /\.[^/]+$/.test(url.pathname);
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');

return net.fetch(pathToFileURL(filePath).toString());
});
}

app.whenReady().then(async () => {
registerClerkAppProtocol();
await createWindow();
});

app.on('window-all-closed', () => {
app.quit();
});

app.on('before-quit', () => {
clerk.cleanup();
});
26 changes: 26 additions & 0 deletions integration/templates/electron-vite/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
{
"name": "electron-vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"dependencies": {
"electron-store": "^8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^22.12.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.6",
"typescript": "^5.7.3",
"vite": "^7.3.3"
},
"engines": {
"node": ">=24.15.0"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions integration/templates/electron-vite/preload.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { exposeClerkBridge } from '@clerk/electron/preload';

exposeClerkBridge();
46 changes: 46 additions & 0 deletions integration/templates/electron-vite/src/main.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
import React from 'react';
import ReactDOM from 'react-dom/client';

import './style.css';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the required Vite env vars at startup.

The as string assertions only silence TypeScript; an unset VITE_CLERK_* still flows through as undefined and fails later inside Clerk initialization. Throw a clear startup error here instead.

🛠️ Proposed change
-const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;-const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;+const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL;++if (!PUBLISHABLE_KEY || !CLERK_UI_URL) {+ throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');+}

As per coding guidelines, "Validate all inputs and sanitize outputs" and "Provide meaningful error messages to developers".

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEYasstring;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URLasstring;
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URL;
if(!PUBLISHABLE_KEY||!CLERK_UI_URL){
thrownewError('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/templates/electron-vite/src/main.tsx` around lines 7 - 8, The
startup constants in main.tsx rely on TypeScript assertions only, so missing
VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL can still reach Clerk as
undefined. Update the initialization around PUBLISHABLE_KEY and CLERK_UI_URL to
validate both env vars immediately at startup and throw a clear,
developer-facing error if either is absent or empty. Keep the check close to the
existing env reads so the failure happens before Clerk setup, and make the
message specific enough to identify which required variable is missing.

Source: Coding guidelines


function App() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
__internal_clerkUIUrl={CLERK_UI_URL}
routerPush={() => {}}
routerReplace={() => {}}
>
<main data-testid='electron-app'>
<Show when='signed-out'>
<SignIn />
</Show>
<Show when='signed-in'>
<UserButton />
<AuthInfo />
</Show>
</main>
</ClerkProvider>
);
}

function AuthInfo() {
const { sessionId, userId } = useAuth();

return (
<div>
<p data-testid='user-id'>{userId}</p>
<p data-testid='session-id'>{sessionId}</p>
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
25 changes: 25 additions & 0 deletions integration/templates/electron-vite/src/style.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
background: #f8fafc;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
}
1 change: 1 addition & 0 deletions integration/templates/electron-vite/src/vite-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions integration/templates/electron-vite/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions integration/templates/electron-vite/vite.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const templates = {
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'chrome-extension-vite': resolve(__dirname, './chrome-extension-vite'),
'electron-vite': resolve(__dirname, './electron-vite'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
77 changes: 77 additions & 0 deletions integration/tests/electron/basic.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import { clerk } from '@clerk/testing/playwright';
import { createPageObjects } from '@clerk/testing/playwright/unstable';

import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { expect, test } from './fixtures';

type ElectronWindow = Window & {
__clerk_internal_electron?: {
tokenCache?: Partial<Record<'clearToken' | 'getToken' | 'saveToken', unknown>>;
oauthTransport?: Partial<Record<'getRedirectUrl' | 'open', unknown>>;
};
};

test.describe('electron basic auth @electron', () => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async ({ electronTestApp }) => {
const u = createTestUtils({ app: electronTestApp });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser?.deleteIfExists();
});

test('exposes the preload bridge to the renderer', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="electron-app"]')).toBeVisible();

await expect(
electronPage.evaluate(() => {
const bridge = (window as ElectronWindow).__clerk_internal_electron;

return (
typeof bridge?.tokenCache?.clearToken === 'function' &&
typeof bridge?.tokenCache?.getToken === 'function' &&
typeof bridge?.tokenCache?.saveToken === 'function' &&
typeof bridge?.oauthTransport?.getRedirectUrl === 'function' &&
typeof bridge?.oauthTransport?.open === 'function'
);
}),
).resolves.toBe(true);
});

test('signs in with email and password', async ({ electronPage }) => {
const { signIn } = createPageObjects({ page: electronPage, useTestingToken: false });

await signIn.waitForMounted();
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible();

await signIn.setIdentifier(fakeUser.email!);
await signIn.continue();
await signIn.setPassword(fakeUser.password);
await signIn.continue();

await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
});

test('persists the signed-in session after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
await expect(electronPage.locator('[data-testid="session-id"]')).toHaveText(/^sess_/, { timeout: 30_000 });
});

test('signs out and clears the session', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-userButtonTrigger')).toBeVisible({ timeout: 30_000 });
await clerk.signOut({ page: electronPage });

await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});

test('keeps the signed-out state after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/electron-e2e-tests.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,7 @@ jobs:
"custom",
"hono",
"chrome-extension",
"electron",
]
test-project: ["chrome"]
include:
Expand DownExpand Up@@ -486,10 +487,23 @@ jobs:
working-directory: ./integration/certs
run: ls -la && pwd

- name: Ensure Xvfb is installed
if: ${{ matrix.test-name == 'electron' }}
run: |
if ! command -v xvfb-run &> /dev/null; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
run: |
if [ "${{ matrix.test-name }}" = "electron" ]; then
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
else
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
E2E_DEBUG: "1"
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
Expand Down
17 changes: 17 additions & 0 deletions integration/presets/electron.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { PKGLAB } from './utils';

const vite = applicationConfig()
.setName('electron-vite')
.useTemplate(templates['electron-vite'])
.setEnvFormatter('public', key => `VITE_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'echo noop')
.addScript('build', 'pnpm build')
.addScript('serve', 'echo noop')
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire dev and serve to real commands.

pnpm dev currently just performs a build, and pnpm serve exits immediately, so the generated Electron template has no runnable development or start path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/presets/electron.ts` around lines 9 - 12, The Electron preset
currently wires the package scripts in `integration/presets/electron.ts` so
`dev` only builds and `serve` is a noop, which leaves no runnable app flow.
Update the `addScript('dev', ...)` and `addScript('serve', ...)` entries to
point to the actual Electron development and startup commands used by the
template, keeping `setup` and `build` unchanged. Use the existing script wiring
in the preset as the place to fix this so generated projects expose a real `dev`
path and a real `serve` path.

.addDependency('@clerk/electron', PKGLAB);

export const electron = {
vite,
} as const;
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { astro } from './astro';
import { chromeExtension } from './chrome-extension';
import { customFlows } from './custom-flows';
import { electron } from './electron';
import { envs, instanceKeys } from './envs';
import { expo } from './expo';
import { express } from './express';
Expand All@@ -17,6 +18,7 @@ import { vue } from './vue';
export const appConfigs = {
chromeExtension,
customFlows,
electron,
envs,
express,
fastify,
Expand Down
18 changes: 18 additions & 0 deletions integration/templates/electron-vite/index.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Clerk Electron E2E</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
77 changes: 77 additions & 0 deletions integration/templates/electron-vite/main.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { createClerkBridge } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';
import { app, BrowserWindow, net, protocol } from 'electron';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RENDERER_SCHEME = 'clerk';
const RENDERER_HOST = 'app';
const rendererRoot = path.resolve(__dirname, 'dist');

const clerk = createClerkBridge({
storage: storage({ unencryptedFallback: true }),
renderer: {
scheme: RENDERER_SCHEME,
host: RENDERER_HOST,
},
});

async function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.mjs'),
sandbox: false,
},
});

await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
}

function registerClerkAppProtocol() {
protocol.handle(RENDERER_SCHEME, async request => {
const url = new URL(request.url);

if (url.host !== RENDERER_HOST) {
return new Response('Not found', { status: 404 });
}

let requestedPath;
try {
requestedPath = decodeURIComponent(url.pathname);
} catch {
return new Response('Bad request', { status: 400 });
}

const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
const relativePath = path.relative(rendererRoot, resolvedPath);
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));

if (!isSafe) {
return new Response('Forbidden', { status: 403 });
}

const hasExtension = /\.[^/]+$/.test(url.pathname);
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');

return net.fetch(pathToFileURL(filePath).toString());
});
}

app.whenReady().then(async () => {
registerClerkAppProtocol();
await createWindow();
});

app.on('window-all-closed', () => {
app.quit();
});

app.on('before-quit', () => {
clerk.cleanup();
});
26 changes: 26 additions & 0 deletions integration/templates/electron-vite/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
{
"name": "electron-vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"dependencies": {
"electron-store": "^8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^22.12.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.6",
"typescript": "^5.7.3",
"vite": "^7.3.3"
},
"engines": {
"node": ">=24.15.0"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions integration/templates/electron-vite/preload.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { exposeClerkBridge } from '@clerk/electron/preload';

exposeClerkBridge();
46 changes: 46 additions & 0 deletions integration/templates/electron-vite/src/main.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
import React from 'react';
import ReactDOM from 'react-dom/client';

import './style.css';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the required Vite env vars at startup.

The as string assertions only silence TypeScript; an unset VITE_CLERK_* still flows through as undefined and fails later inside Clerk initialization. Throw a clear startup error here instead.

🛠️ Proposed change
-const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;-const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;+const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL;++if (!PUBLISHABLE_KEY || !CLERK_UI_URL) {+ throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');+}

As per coding guidelines, "Validate all inputs and sanitize outputs" and "Provide meaningful error messages to developers".

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEYasstring;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URLasstring;
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URL;
if(!PUBLISHABLE_KEY||!CLERK_UI_URL){
thrownewError('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/templates/electron-vite/src/main.tsx` around lines 7 - 8, The
startup constants in main.tsx rely on TypeScript assertions only, so missing
VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL can still reach Clerk as
undefined. Update the initialization around PUBLISHABLE_KEY and CLERK_UI_URL to
validate both env vars immediately at startup and throw a clear,
developer-facing error if either is absent or empty. Keep the check close to the
existing env reads so the failure happens before Clerk setup, and make the
message specific enough to identify which required variable is missing.

Source: Coding guidelines


function App() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
__internal_clerkUIUrl={CLERK_UI_URL}
routerPush={() => {}}
routerReplace={() => {}}
>
<main data-testid='electron-app'>
<Show when='signed-out'>
<SignIn />
</Show>
<Show when='signed-in'>
<UserButton />
<AuthInfo />
</Show>
</main>
</ClerkProvider>
);
}

function AuthInfo() {
const { sessionId, userId } = useAuth();

return (
<div>
<p data-testid='user-id'>{userId}</p>
<p data-testid='session-id'>{sessionId}</p>
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
25 changes: 25 additions & 0 deletions integration/templates/electron-vite/src/style.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
background: #f8fafc;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
}
1 change: 1 addition & 0 deletions integration/templates/electron-vite/src/vite-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions integration/templates/electron-vite/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions integration/templates/electron-vite/vite.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const templates = {
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'chrome-extension-vite': resolve(__dirname, './chrome-extension-vite'),
'electron-vite': resolve(__dirname, './electron-vite'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
77 changes: 77 additions & 0 deletions integration/tests/electron/basic.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import { clerk } from '@clerk/testing/playwright';
import { createPageObjects } from '@clerk/testing/playwright/unstable';

import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { expect, test } from './fixtures';

type ElectronWindow = Window & {
__clerk_internal_electron?: {
tokenCache?: Partial<Record<'clearToken' | 'getToken' | 'saveToken', unknown>>;
oauthTransport?: Partial<Record<'getRedirectUrl' | 'open', unknown>>;
};
};

test.describe('electron basic auth @electron', () => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async ({ electronTestApp }) => {
const u = createTestUtils({ app: electronTestApp });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser?.deleteIfExists();
});

test('exposes the preload bridge to the renderer', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="electron-app"]')).toBeVisible();

await expect(
electronPage.evaluate(() => {
const bridge = (window as ElectronWindow).__clerk_internal_electron;

return (
typeof bridge?.tokenCache?.clearToken === 'function' &&
typeof bridge?.tokenCache?.getToken === 'function' &&
typeof bridge?.tokenCache?.saveToken === 'function' &&
typeof bridge?.oauthTransport?.getRedirectUrl === 'function' &&
typeof bridge?.oauthTransport?.open === 'function'
);
}),
).resolves.toBe(true);
});

test('signs in with email and password', async ({ electronPage }) => {
const { signIn } = createPageObjects({ page: electronPage, useTestingToken: false });

await signIn.waitForMounted();
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible();

await signIn.setIdentifier(fakeUser.email!);
await signIn.continue();
await signIn.setPassword(fakeUser.password);
await signIn.continue();

await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
});

test('persists the signed-in session after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
await expect(electronPage.locator('[data-testid="session-id"]')).toHaveText(/^sess_/, { timeout: 30_000 });
});

test('signs out and clears the session', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-userButtonTrigger')).toBeVisible({ timeout: 30_000 });
await clerk.signOut({ page: electronPage });

await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});

test('keeps the signed-out state after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/electron-e2e-tests.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,7 @@ jobs:
"custom",
"hono",
"chrome-extension",
"electron",
]
test-project: ["chrome"]
include:
Expand DownExpand Up@@ -486,10 +487,23 @@ jobs:
working-directory: ./integration/certs
run: ls -la && pwd

- name: Ensure Xvfb is installed
if: ${{ matrix.test-name == 'electron' }}
run: |
if ! command -v xvfb-run &> /dev/null; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
run: |
if [ "${{ matrix.test-name }}" = "electron" ]; then
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
else
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
E2E_DEBUG: "1"
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
Expand Down
17 changes: 17 additions & 0 deletions integration/presets/electron.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { PKGLAB } from './utils';

const vite = applicationConfig()
.setName('electron-vite')
.useTemplate(templates['electron-vite'])
.setEnvFormatter('public', key => `VITE_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'echo noop')
.addScript('build', 'pnpm build')
.addScript('serve', 'echo noop')
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire dev and serve to real commands.

pnpm dev currently just performs a build, and pnpm serve exits immediately, so the generated Electron template has no runnable development or start path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/presets/electron.ts` around lines 9 - 12, The Electron preset
currently wires the package scripts in `integration/presets/electron.ts` so
`dev` only builds and `serve` is a noop, which leaves no runnable app flow.
Update the `addScript('dev', ...)` and `addScript('serve', ...)` entries to
point to the actual Electron development and startup commands used by the
template, keeping `setup` and `build` unchanged. Use the existing script wiring
in the preset as the place to fix this so generated projects expose a real `dev`
path and a real `serve` path.

.addDependency('@clerk/electron', PKGLAB);

export const electron = {
vite,
} as const;
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { astro } from './astro';
import { chromeExtension } from './chrome-extension';
import { customFlows } from './custom-flows';
import { electron } from './electron';
import { envs, instanceKeys } from './envs';
import { expo } from './expo';
import { express } from './express';
Expand All@@ -17,6 +18,7 @@ import { vue } from './vue';
export const appConfigs = {
chromeExtension,
customFlows,
electron,
envs,
express,
fastify,
Expand Down
18 changes: 18 additions & 0 deletions integration/templates/electron-vite/index.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Clerk Electron E2E</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
77 changes: 77 additions & 0 deletions integration/templates/electron-vite/main.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { createClerkBridge } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';
import { app, BrowserWindow, net, protocol } from 'electron';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RENDERER_SCHEME = 'clerk';
const RENDERER_HOST = 'app';
const rendererRoot = path.resolve(__dirname, 'dist');

const clerk = createClerkBridge({
storage: storage({ unencryptedFallback: true }),
renderer: {
scheme: RENDERER_SCHEME,
host: RENDERER_HOST,
},
});

async function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.mjs'),
sandbox: false,
},
});

await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
}

function registerClerkAppProtocol() {
protocol.handle(RENDERER_SCHEME, async request => {
const url = new URL(request.url);

if (url.host !== RENDERER_HOST) {
return new Response('Not found', { status: 404 });
}

let requestedPath;
try {
requestedPath = decodeURIComponent(url.pathname);
} catch {
return new Response('Bad request', { status: 400 });
}

const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
const relativePath = path.relative(rendererRoot, resolvedPath);
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));

if (!isSafe) {
return new Response('Forbidden', { status: 403 });
}

const hasExtension = /\.[^/]+$/.test(url.pathname);
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');

return net.fetch(pathToFileURL(filePath).toString());
});
}

app.whenReady().then(async () => {
registerClerkAppProtocol();
await createWindow();
});

app.on('window-all-closed', () => {
app.quit();
});

app.on('before-quit', () => {
clerk.cleanup();
});
26 changes: 26 additions & 0 deletions integration/templates/electron-vite/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
{
"name": "electron-vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"dependencies": {
"electron-store": "^8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^22.12.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.6",
"typescript": "^5.7.3",
"vite": "^7.3.3"
},
"engines": {
"node": ">=24.15.0"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions integration/templates/electron-vite/preload.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { exposeClerkBridge } from '@clerk/electron/preload';

exposeClerkBridge();
46 changes: 46 additions & 0 deletions integration/templates/electron-vite/src/main.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
import React from 'react';
import ReactDOM from 'react-dom/client';

import './style.css';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the required Vite env vars at startup.

The as string assertions only silence TypeScript; an unset VITE_CLERK_* still flows through as undefined and fails later inside Clerk initialization. Throw a clear startup error here instead.

🛠️ Proposed change
-const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;-const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;+const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL;++if (!PUBLISHABLE_KEY || !CLERK_UI_URL) {+ throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');+}

As per coding guidelines, "Validate all inputs and sanitize outputs" and "Provide meaningful error messages to developers".

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEYasstring;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URLasstring;
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URL;
if(!PUBLISHABLE_KEY||!CLERK_UI_URL){
thrownewError('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/templates/electron-vite/src/main.tsx` around lines 7 - 8, The
startup constants in main.tsx rely on TypeScript assertions only, so missing
VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL can still reach Clerk as
undefined. Update the initialization around PUBLISHABLE_KEY and CLERK_UI_URL to
validate both env vars immediately at startup and throw a clear,
developer-facing error if either is absent or empty. Keep the check close to the
existing env reads so the failure happens before Clerk setup, and make the
message specific enough to identify which required variable is missing.

Source: Coding guidelines


function App() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
__internal_clerkUIUrl={CLERK_UI_URL}
routerPush={() => {}}
routerReplace={() => {}}
>
<main data-testid='electron-app'>
<Show when='signed-out'>
<SignIn />
</Show>
<Show when='signed-in'>
<UserButton />
<AuthInfo />
</Show>
</main>
</ClerkProvider>
);
}

function AuthInfo() {
const { sessionId, userId } = useAuth();

return (
<div>
<p data-testid='user-id'>{userId}</p>
<p data-testid='session-id'>{sessionId}</p>
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
25 changes: 25 additions & 0 deletions integration/templates/electron-vite/src/style.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
background: #f8fafc;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
}
1 change: 1 addition & 0 deletions integration/templates/electron-vite/src/vite-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions integration/templates/electron-vite/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions integration/templates/electron-vite/vite.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const templates = {
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'chrome-extension-vite': resolve(__dirname, './chrome-extension-vite'),
'electron-vite': resolve(__dirname, './electron-vite'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
77 changes: 77 additions & 0 deletions integration/tests/electron/basic.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import { clerk } from '@clerk/testing/playwright';
import { createPageObjects } from '@clerk/testing/playwright/unstable';

import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { expect, test } from './fixtures';

type ElectronWindow = Window & {
__clerk_internal_electron?: {
tokenCache?: Partial<Record<'clearToken' | 'getToken' | 'saveToken', unknown>>;
oauthTransport?: Partial<Record<'getRedirectUrl' | 'open', unknown>>;
};
};

test.describe('electron basic auth @electron', () => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async ({ electronTestApp }) => {
const u = createTestUtils({ app: electronTestApp });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser?.deleteIfExists();
});

test('exposes the preload bridge to the renderer', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="electron-app"]')).toBeVisible();

await expect(
electronPage.evaluate(() => {
const bridge = (window as ElectronWindow).__clerk_internal_electron;

return (
typeof bridge?.tokenCache?.clearToken === 'function' &&
typeof bridge?.tokenCache?.getToken === 'function' &&
typeof bridge?.tokenCache?.saveToken === 'function' &&
typeof bridge?.oauthTransport?.getRedirectUrl === 'function' &&
typeof bridge?.oauthTransport?.open === 'function'
);
}),
).resolves.toBe(true);
});

test('signs in with email and password', async ({ electronPage }) => {
const { signIn } = createPageObjects({ page: electronPage, useTestingToken: false });

await signIn.waitForMounted();
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible();

await signIn.setIdentifier(fakeUser.email!);
await signIn.continue();
await signIn.setPassword(fakeUser.password);
await signIn.continue();

await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
});

test('persists the signed-in session after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
await expect(electronPage.locator('[data-testid="session-id"]')).toHaveText(/^sess_/, { timeout: 30_000 });
});

test('signs out and clears the session', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-userButtonTrigger')).toBeVisible({ timeout: 30_000 });
await clerk.signOut({ page: electronPage });

await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});

test('keeps the signed-out state after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/electron-e2e-tests.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,7 @@ jobs:
"custom",
"hono",
"chrome-extension",
"electron",
]
test-project: ["chrome"]
include:
Expand DownExpand Up@@ -486,10 +487,23 @@ jobs:
working-directory: ./integration/certs
run: ls -la && pwd

- name: Ensure Xvfb is installed
if: ${{ matrix.test-name == 'electron' }}
run: |
if ! command -v xvfb-run &> /dev/null; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
run: |
if [ "${{ matrix.test-name }}" = "electron" ]; then
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
else
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
E2E_DEBUG: "1"
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
Expand Down
17 changes: 17 additions & 0 deletions integration/presets/electron.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { PKGLAB } from './utils';

const vite = applicationConfig()
.setName('electron-vite')
.useTemplate(templates['electron-vite'])
.setEnvFormatter('public', key => `VITE_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'echo noop')
.addScript('build', 'pnpm build')
.addScript('serve', 'echo noop')
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire dev and serve to real commands.

pnpm dev currently just performs a build, and pnpm serve exits immediately, so the generated Electron template has no runnable development or start path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/presets/electron.ts` around lines 9 - 12, The Electron preset
currently wires the package scripts in `integration/presets/electron.ts` so
`dev` only builds and `serve` is a noop, which leaves no runnable app flow.
Update the `addScript('dev', ...)` and `addScript('serve', ...)` entries to
point to the actual Electron development and startup commands used by the
template, keeping `setup` and `build` unchanged. Use the existing script wiring
in the preset as the place to fix this so generated projects expose a real `dev`
path and a real `serve` path.

.addDependency('@clerk/electron', PKGLAB);

export const electron = {
vite,
} as const;
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { astro } from './astro';
import { chromeExtension } from './chrome-extension';
import { customFlows } from './custom-flows';
import { electron } from './electron';
import { envs, instanceKeys } from './envs';
import { expo } from './expo';
import { express } from './express';
Expand All@@ -17,6 +18,7 @@ import { vue } from './vue';
export const appConfigs = {
chromeExtension,
customFlows,
electron,
envs,
express,
fastify,
Expand Down
18 changes: 18 additions & 0 deletions integration/templates/electron-vite/index.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Clerk Electron E2E</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
77 changes: 77 additions & 0 deletions integration/templates/electron-vite/main.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { createClerkBridge } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';
import { app, BrowserWindow, net, protocol } from 'electron';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RENDERER_SCHEME = 'clerk';
const RENDERER_HOST = 'app';
const rendererRoot = path.resolve(__dirname, 'dist');

const clerk = createClerkBridge({
storage: storage({ unencryptedFallback: true }),
renderer: {
scheme: RENDERER_SCHEME,
host: RENDERER_HOST,
},
});

async function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.mjs'),
sandbox: false,
},
});

await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
}

function registerClerkAppProtocol() {
protocol.handle(RENDERER_SCHEME, async request => {
const url = new URL(request.url);

if (url.host !== RENDERER_HOST) {
return new Response('Not found', { status: 404 });
}

let requestedPath;
try {
requestedPath = decodeURIComponent(url.pathname);
} catch {
return new Response('Bad request', { status: 400 });
}

const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
const relativePath = path.relative(rendererRoot, resolvedPath);
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));

if (!isSafe) {
return new Response('Forbidden', { status: 403 });
}

const hasExtension = /\.[^/]+$/.test(url.pathname);
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');

return net.fetch(pathToFileURL(filePath).toString());
});
}

app.whenReady().then(async () => {
registerClerkAppProtocol();
await createWindow();
});

app.on('window-all-closed', () => {
app.quit();
});

app.on('before-quit', () => {
clerk.cleanup();
});
26 changes: 26 additions & 0 deletions integration/templates/electron-vite/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
{
"name": "electron-vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"dependencies": {
"electron-store": "^8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^22.12.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.6",
"typescript": "^5.7.3",
"vite": "^7.3.3"
},
"engines": {
"node": ">=24.15.0"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions integration/templates/electron-vite/preload.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { exposeClerkBridge } from '@clerk/electron/preload';

exposeClerkBridge();
46 changes: 46 additions & 0 deletions integration/templates/electron-vite/src/main.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
import React from 'react';
import ReactDOM from 'react-dom/client';

import './style.css';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the required Vite env vars at startup.

The as string assertions only silence TypeScript; an unset VITE_CLERK_* still flows through as undefined and fails later inside Clerk initialization. Throw a clear startup error here instead.

🛠️ Proposed change
-const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;-const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;+const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL;++if (!PUBLISHABLE_KEY || !CLERK_UI_URL) {+ throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');+}

As per coding guidelines, "Validate all inputs and sanitize outputs" and "Provide meaningful error messages to developers".

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEYasstring;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URLasstring;
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URL;
if(!PUBLISHABLE_KEY||!CLERK_UI_URL){
thrownewError('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/templates/electron-vite/src/main.tsx` around lines 7 - 8, The
startup constants in main.tsx rely on TypeScript assertions only, so missing
VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL can still reach Clerk as
undefined. Update the initialization around PUBLISHABLE_KEY and CLERK_UI_URL to
validate both env vars immediately at startup and throw a clear,
developer-facing error if either is absent or empty. Keep the check close to the
existing env reads so the failure happens before Clerk setup, and make the
message specific enough to identify which required variable is missing.

Source: Coding guidelines


function App() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
__internal_clerkUIUrl={CLERK_UI_URL}
routerPush={() => {}}
routerReplace={() => {}}
>
<main data-testid='electron-app'>
<Show when='signed-out'>
<SignIn />
</Show>
<Show when='signed-in'>
<UserButton />
<AuthInfo />
</Show>
</main>
</ClerkProvider>
);
}

function AuthInfo() {
const { sessionId, userId } = useAuth();

return (
<div>
<p data-testid='user-id'>{userId}</p>
<p data-testid='session-id'>{sessionId}</p>
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
25 changes: 25 additions & 0 deletions integration/templates/electron-vite/src/style.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
background: #f8fafc;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
}
1 change: 1 addition & 0 deletions integration/templates/electron-vite/src/vite-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions integration/templates/electron-vite/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions integration/templates/electron-vite/vite.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const templates = {
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'chrome-extension-vite': resolve(__dirname, './chrome-extension-vite'),
'electron-vite': resolve(__dirname, './electron-vite'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
77 changes: 77 additions & 0 deletions integration/tests/electron/basic.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import { clerk } from '@clerk/testing/playwright';
import { createPageObjects } from '@clerk/testing/playwright/unstable';

import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { expect, test } from './fixtures';

type ElectronWindow = Window & {
__clerk_internal_electron?: {
tokenCache?: Partial<Record<'clearToken' | 'getToken' | 'saveToken', unknown>>;
oauthTransport?: Partial<Record<'getRedirectUrl' | 'open', unknown>>;
};
};

test.describe('electron basic auth @electron', () => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async ({ electronTestApp }) => {
const u = createTestUtils({ app: electronTestApp });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser?.deleteIfExists();
});

test('exposes the preload bridge to the renderer', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="electron-app"]')).toBeVisible();

await expect(
electronPage.evaluate(() => {
const bridge = (window as ElectronWindow).__clerk_internal_electron;

return (
typeof bridge?.tokenCache?.clearToken === 'function' &&
typeof bridge?.tokenCache?.getToken === 'function' &&
typeof bridge?.tokenCache?.saveToken === 'function' &&
typeof bridge?.oauthTransport?.getRedirectUrl === 'function' &&
typeof bridge?.oauthTransport?.open === 'function'
);
}),
).resolves.toBe(true);
});

test('signs in with email and password', async ({ electronPage }) => {
const { signIn } = createPageObjects({ page: electronPage, useTestingToken: false });

await signIn.waitForMounted();
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible();

await signIn.setIdentifier(fakeUser.email!);
await signIn.continue();
await signIn.setPassword(fakeUser.password);
await signIn.continue();

await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
});

test('persists the signed-in session after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
await expect(electronPage.locator('[data-testid="session-id"]')).toHaveText(/^sess_/, { timeout: 30_000 });
});

test('signs out and clears the session', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-userButtonTrigger')).toBeVisible({ timeout: 30_000 });
await clerk.signOut({ page: electronPage });

await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});

test('keeps the signed-out state after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/electron-e2e-tests.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,7 @@ jobs:
"custom",
"hono",
"chrome-extension",
"electron",
]
test-project: ["chrome"]
include:
Expand DownExpand Up@@ -486,10 +487,23 @@ jobs:
working-directory: ./integration/certs
run: ls -la && pwd

- name: Ensure Xvfb is installed
if: ${{ matrix.test-name == 'electron' }}
run: |
if ! command -v xvfb-run &> /dev/null; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
run: |
if [ "${{ matrix.test-name }}" = "electron" ]; then
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
else
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
E2E_DEBUG: "1"
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
Expand Down
17 changes: 17 additions & 0 deletions integration/presets/electron.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { PKGLAB } from './utils';

const vite = applicationConfig()
.setName('electron-vite')
.useTemplate(templates['electron-vite'])
.setEnvFormatter('public', key => `VITE_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'echo noop')
.addScript('build', 'pnpm build')
.addScript('serve', 'echo noop')
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire dev and serve to real commands.

pnpm dev currently just performs a build, and pnpm serve exits immediately, so the generated Electron template has no runnable development or start path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/presets/electron.ts` around lines 9 - 12, The Electron preset
currently wires the package scripts in `integration/presets/electron.ts` so
`dev` only builds and `serve` is a noop, which leaves no runnable app flow.
Update the `addScript('dev', ...)` and `addScript('serve', ...)` entries to
point to the actual Electron development and startup commands used by the
template, keeping `setup` and `build` unchanged. Use the existing script wiring
in the preset as the place to fix this so generated projects expose a real `dev`
path and a real `serve` path.

.addDependency('@clerk/electron', PKGLAB);

export const electron = {
vite,
} as const;
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { astro } from './astro';
import { chromeExtension } from './chrome-extension';
import { customFlows } from './custom-flows';
import { electron } from './electron';
import { envs, instanceKeys } from './envs';
import { expo } from './expo';
import { express } from './express';
Expand All@@ -17,6 +18,7 @@ import { vue } from './vue';
export const appConfigs = {
chromeExtension,
customFlows,
electron,
envs,
express,
fastify,
Expand Down
18 changes: 18 additions & 0 deletions integration/templates/electron-vite/index.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Clerk Electron E2E</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
77 changes: 77 additions & 0 deletions integration/templates/electron-vite/main.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { createClerkBridge } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';
import { app, BrowserWindow, net, protocol } from 'electron';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RENDERER_SCHEME = 'clerk';
const RENDERER_HOST = 'app';
const rendererRoot = path.resolve(__dirname, 'dist');

const clerk = createClerkBridge({
storage: storage({ unencryptedFallback: true }),
renderer: {
scheme: RENDERER_SCHEME,
host: RENDERER_HOST,
},
});

async function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.mjs'),
sandbox: false,
},
});

await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
}

function registerClerkAppProtocol() {
protocol.handle(RENDERER_SCHEME, async request => {
const url = new URL(request.url);

if (url.host !== RENDERER_HOST) {
return new Response('Not found', { status: 404 });
}

let requestedPath;
try {
requestedPath = decodeURIComponent(url.pathname);
} catch {
return new Response('Bad request', { status: 400 });
}

const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
const relativePath = path.relative(rendererRoot, resolvedPath);
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));

if (!isSafe) {
return new Response('Forbidden', { status: 403 });
}

const hasExtension = /\.[^/]+$/.test(url.pathname);
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');

return net.fetch(pathToFileURL(filePath).toString());
});
}

app.whenReady().then(async () => {
registerClerkAppProtocol();
await createWindow();
});

app.on('window-all-closed', () => {
app.quit();
});

app.on('before-quit', () => {
clerk.cleanup();
});
26 changes: 26 additions & 0 deletions integration/templates/electron-vite/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
{
"name": "electron-vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"dependencies": {
"electron-store": "^8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^22.12.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.6",
"typescript": "^5.7.3",
"vite": "^7.3.3"
},
"engines": {
"node": ">=24.15.0"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions integration/templates/electron-vite/preload.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { exposeClerkBridge } from '@clerk/electron/preload';

exposeClerkBridge();
46 changes: 46 additions & 0 deletions integration/templates/electron-vite/src/main.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
import React from 'react';
import ReactDOM from 'react-dom/client';

import './style.css';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the required Vite env vars at startup.

The as string assertions only silence TypeScript; an unset VITE_CLERK_* still flows through as undefined and fails later inside Clerk initialization. Throw a clear startup error here instead.

🛠️ Proposed change
-const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;-const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;+const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL;++if (!PUBLISHABLE_KEY || !CLERK_UI_URL) {+ throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');+}

As per coding guidelines, "Validate all inputs and sanitize outputs" and "Provide meaningful error messages to developers".

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEYasstring;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URLasstring;
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URL;
if(!PUBLISHABLE_KEY||!CLERK_UI_URL){
thrownewError('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/templates/electron-vite/src/main.tsx` around lines 7 - 8, The
startup constants in main.tsx rely on TypeScript assertions only, so missing
VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL can still reach Clerk as
undefined. Update the initialization around PUBLISHABLE_KEY and CLERK_UI_URL to
validate both env vars immediately at startup and throw a clear,
developer-facing error if either is absent or empty. Keep the check close to the
existing env reads so the failure happens before Clerk setup, and make the
message specific enough to identify which required variable is missing.

Source: Coding guidelines


function App() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
__internal_clerkUIUrl={CLERK_UI_URL}
routerPush={() => {}}
routerReplace={() => {}}
>
<main data-testid='electron-app'>
<Show when='signed-out'>
<SignIn />
</Show>
<Show when='signed-in'>
<UserButton />
<AuthInfo />
</Show>
</main>
</ClerkProvider>
);
}

function AuthInfo() {
const { sessionId, userId } = useAuth();

return (
<div>
<p data-testid='user-id'>{userId}</p>
<p data-testid='session-id'>{sessionId}</p>
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
25 changes: 25 additions & 0 deletions integration/templates/electron-vite/src/style.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
background: #f8fafc;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
}
1 change: 1 addition & 0 deletions integration/templates/electron-vite/src/vite-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions integration/templates/electron-vite/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions integration/templates/electron-vite/vite.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const templates = {
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'chrome-extension-vite': resolve(__dirname, './chrome-extension-vite'),
'electron-vite': resolve(__dirname, './electron-vite'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
77 changes: 77 additions & 0 deletions integration/tests/electron/basic.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import { clerk } from '@clerk/testing/playwright';
import { createPageObjects } from '@clerk/testing/playwright/unstable';

import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { expect, test } from './fixtures';

type ElectronWindow = Window & {
__clerk_internal_electron?: {
tokenCache?: Partial<Record<'clearToken' | 'getToken' | 'saveToken', unknown>>;
oauthTransport?: Partial<Record<'getRedirectUrl' | 'open', unknown>>;
};
};

test.describe('electron basic auth @electron', () => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async ({ electronTestApp }) => {
const u = createTestUtils({ app: electronTestApp });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser?.deleteIfExists();
});

test('exposes the preload bridge to the renderer', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="electron-app"]')).toBeVisible();

await expect(
electronPage.evaluate(() => {
const bridge = (window as ElectronWindow).__clerk_internal_electron;

return (
typeof bridge?.tokenCache?.clearToken === 'function' &&
typeof bridge?.tokenCache?.getToken === 'function' &&
typeof bridge?.tokenCache?.saveToken === 'function' &&
typeof bridge?.oauthTransport?.getRedirectUrl === 'function' &&
typeof bridge?.oauthTransport?.open === 'function'
);
}),
).resolves.toBe(true);
});

test('signs in with email and password', async ({ electronPage }) => {
const { signIn } = createPageObjects({ page: electronPage, useTestingToken: false });

await signIn.waitForMounted();
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible();

await signIn.setIdentifier(fakeUser.email!);
await signIn.continue();
await signIn.setPassword(fakeUser.password);
await signIn.continue();

await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
});

test('persists the signed-in session after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
await expect(electronPage.locator('[data-testid="session-id"]')).toHaveText(/^sess_/, { timeout: 30_000 });
});

test('signs out and clears the session', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-userButtonTrigger')).toBeVisible({ timeout: 30_000 });
await clerk.signOut({ page: electronPage });

await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});

test('keeps the signed-out state after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/electron-e2e-tests.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
---
---
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -347,6 +347,7 @@ jobs:
"custom",
"hono",
"chrome-extension",
"electron",
]
test-project: ["chrome"]
include:
Expand DownExpand Up@@ -486,10 +487,23 @@ jobs:
working-directory: ./integration/certs
run: ls -la && pwd

- name: Ensure Xvfb is installed
if: ${{ matrix.test-name == 'electron' }}
run: |
if ! command -v xvfb-run &> /dev/null; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run Integration Tests
id: integration-tests
timeout-minutes: 25
run: pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
run: |
if [ "${{ matrix.test-name }}" = "electron" ]; then
xvfb-run -a pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
else
pnpm turbo test:integration:${{ matrix.test-name }} $TURBO_ARGS
fi
env:
E2E_DEBUG: "1"
E2E_APP_CLERK_JS_DIR: ${{runner.temp}}
Expand Down
17 changes: 17 additions & 0 deletions integration/presets/electron.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
import { applicationConfig } from '../models/applicationConfig';
import { templates } from '../templates';
import { PKGLAB } from './utils';

const vite = applicationConfig()
.setName('electron-vite')
.useTemplate(templates['electron-vite'])
.setEnvFormatter('public', key => `VITE_${key}`)
.addScript('setup', 'pnpm install')
.addScript('dev', 'echo noop')
.addScript('build', 'pnpm build')
.addScript('serve', 'echo noop')
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Wire dev and serve to real commands.

pnpm dev currently just performs a build, and pnpm serve exits immediately, so the generated Electron template has no runnable development or start path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/presets/electron.ts` around lines 9 - 12, The Electron preset
currently wires the package scripts in `integration/presets/electron.ts` so
`dev` only builds and `serve` is a noop, which leaves no runnable app flow.
Update the `addScript('dev', ...)` and `addScript('serve', ...)` entries to
point to the actual Electron development and startup commands used by the
template, keeping `setup` and `build` unchanged. Use the existing script wiring
in the preset as the place to fix this so generated projects expose a real `dev`
path and a real `serve` path.

.addDependency('@clerk/electron', PKGLAB);

export const electron = {
vite,
} as const;
2 changes: 2 additions & 0 deletions integration/presets/index.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { astro } from './astro';
import { chromeExtension } from './chrome-extension';
import { customFlows } from './custom-flows';
import { electron } from './electron';
import { envs, instanceKeys } from './envs';
import { expo } from './expo';
import { express } from './express';
Expand All@@ -17,6 +18,7 @@ import { vue } from './vue';
export const appConfigs = {
chromeExtension,
customFlows,
electron,
envs,
express,
fastify,
Expand Down
18 changes: 18 additions & 0 deletions integration/templates/electron-vite/index.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>Clerk Electron E2E</title>
</head>
<body>
<div id="root"></div>
<script
type="module"
src="/src/main.tsx"
></script>
</body>
</html>
77 changes: 77 additions & 0 deletions integration/templates/electron-vite/main.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import { createClerkBridge } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';
import { app, BrowserWindow, net, protocol } from 'electron';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const RENDERER_SCHEME = 'clerk';
const RENDERER_HOST = 'app';
const rendererRoot = path.resolve(__dirname, 'dist');

const clerk = createClerkBridge({
storage: storage({ unencryptedFallback: true }),
renderer: {
scheme: RENDERER_SCHEME,
host: RENDERER_HOST,
},
});

async function createWindow() {
const win = new BrowserWindow({
width: 1000,
height: 800,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload: path.resolve(__dirname, 'preload.mjs'),
sandbox: false,
},
});

await win.loadURL(`${RENDERER_SCHEME}://${RENDERER_HOST}/`);
}

function registerClerkAppProtocol() {
protocol.handle(RENDERER_SCHEME, async request => {
const url = new URL(request.url);

if (url.host !== RENDERER_HOST) {
return new Response('Not found', { status: 404 });
}

let requestedPath;
try {
requestedPath = decodeURIComponent(url.pathname);
} catch {
return new Response('Bad request', { status: 400 });
}

const resolvedPath = path.resolve(rendererRoot, `.${requestedPath}`);
const relativePath = path.relative(rendererRoot, resolvedPath);
const isSafe = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));

if (!isSafe) {
return new Response('Forbidden', { status: 403 });
}

const hasExtension = /\.[^/]+$/.test(url.pathname);
const filePath = hasExtension ? resolvedPath : path.join(rendererRoot, 'index.html');

return net.fetch(pathToFileURL(filePath).toString());
});
}

app.whenReady().then(async () => {
registerClerkAppProtocol();
await createWindow();
});

app.on('window-all-closed', () => {
app.quit();
});

app.on('before-quit', () => {
clerk.cleanup();
});
26 changes: 26 additions & 0 deletions integration/templates/electron-vite/package.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
{
"name": "electron-vite",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
},
"dependencies": {
"electron-store": "^8.2.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
"@types/node": "^22.12.0",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"electron": "^39.2.6",
"typescript": "^5.7.3",
"vite": "^7.3.3"
},
"engines": {
"node": ">=24.15.0"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
3 changes: 3 additions & 0 deletions integration/templates/electron-vite/preload.mjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
import { exposeClerkBridge } from '@clerk/electron/preload';

exposeClerkBridge();
46 changes: 46 additions & 0 deletions integration/templates/electron-vite/src/main.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
import { ClerkProvider, Show, SignIn, UserButton, useAuth } from '@clerk/electron/react';
import React from 'react';
import ReactDOM from 'react-dom/client';

import './style.css';

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;
const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;
Comment on lines +7 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the required Vite env vars at startup.

The as string assertions only silence TypeScript; an unset VITE_CLERK_* still flows through as undefined and fails later inside Clerk initialization. Throw a clear startup error here instead.

🛠️ Proposed change
-const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string;-const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL as string;+const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;+const CLERK_UI_URL = import.meta.env.VITE_CLERK_UI_URL;++if (!PUBLISHABLE_KEY || !CLERK_UI_URL) {+ throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');+}

As per coding guidelines, "Validate all inputs and sanitize outputs" and "Provide meaningful error messages to developers".

📝 Committable suggestion

‼️IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEYasstring;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URLasstring;
constPUBLISHABLE_KEY=import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
constCLERK_UI_URL=import.meta.env.VITE_CLERK_UI_URL;
if(!PUBLISHABLE_KEY||!CLERK_UI_URL){
thrownewError('Missing VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL for the Electron renderer');
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@integration/templates/electron-vite/src/main.tsx` around lines 7 - 8, The
startup constants in main.tsx rely on TypeScript assertions only, so missing
VITE_CLERK_PUBLISHABLE_KEY or VITE_CLERK_UI_URL can still reach Clerk as
undefined. Update the initialization around PUBLISHABLE_KEY and CLERK_UI_URL to
validate both env vars immediately at startup and throw a clear,
developer-facing error if either is absent or empty. Keep the check close to the
existing env reads so the failure happens before Clerk setup, and make the
message specific enough to identify which required variable is missing.

Source: Coding guidelines


function App() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
__internal_clerkUIUrl={CLERK_UI_URL}
routerPush={() => {}}
routerReplace={() => {}}
>
<main data-testid='electron-app'>
<Show when='signed-out'>
<SignIn />
</Show>
<Show when='signed-in'>
<UserButton />
<AuthInfo />
</Show>
</main>
</ClerkProvider>
);
}

function AuthInfo() {
const { sessionId, userId } = useAuth();

return (
<div>
<p data-testid='user-id'>{userId}</p>
<p data-testid='session-id'>{sessionId}</p>
</div>
);
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
25 changes: 25 additions & 0 deletions integration/templates/electron-vite/src/style.css
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
* {
box-sizing: border-box;
}

body {
min-width: 320px;
min-height: 100vh;
margin: 0;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
background: #f8fafc;
}

main {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
}
1 change: 1 addition & 0 deletions integration/templates/electron-vite/src/vite-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
/// <reference types="vite/client" />
20 changes: 20 additions & 0 deletions integration/templates/electron-vite/tsconfig.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "vite.config.ts"]
}
6 changes: 6 additions & 0 deletions integration/templates/electron-vite/vite.config.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
plugins: [react()],
});
1 change: 1 addition & 0 deletions integration/templates/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,6 +24,7 @@ export const templates = {
'react-router-library': resolve(__dirname, './react-router-library'),
'custom-flows-react-vite': resolve(__dirname, './custom-flows-react-vite'),
'chrome-extension-vite': resolve(__dirname, './chrome-extension-vite'),
'electron-vite': resolve(__dirname, './electron-vite'),
} as const;

if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Expand Down
77 changes: 77 additions & 0 deletions integration/tests/electron/basic.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
import { clerk } from '@clerk/testing/playwright';
import { createPageObjects } from '@clerk/testing/playwright/unstable';

import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { expect, test } from './fixtures';

type ElectronWindow = Window & {
__clerk_internal_electron?: {
tokenCache?: Partial<Record<'clearToken' | 'getToken' | 'saveToken', unknown>>;
oauthTransport?: Partial<Record<'getRedirectUrl' | 'open', unknown>>;
};
};

test.describe('electron basic auth @electron', () => {
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;

test.beforeAll(async ({ electronTestApp }) => {
const u = createTestUtils({ app: electronTestApp });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser?.deleteIfExists();
});

test('exposes the preload bridge to the renderer', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="electron-app"]')).toBeVisible();

await expect(
electronPage.evaluate(() => {
const bridge = (window as ElectronWindow).__clerk_internal_electron;

return (
typeof bridge?.tokenCache?.clearToken === 'function' &&
typeof bridge?.tokenCache?.getToken === 'function' &&
typeof bridge?.tokenCache?.saveToken === 'function' &&
typeof bridge?.oauthTransport?.getRedirectUrl === 'function' &&
typeof bridge?.oauthTransport?.open === 'function'
);
}),
).resolves.toBe(true);
});

test('signs in with email and password', async ({ electronPage }) => {
const { signIn } = createPageObjects({ page: electronPage, useTestingToken: false });

await signIn.waitForMounted();
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible();

await signIn.setIdentifier(fakeUser.email!);
await signIn.continue();
await signIn.setPassword(fakeUser.password);
await signIn.continue();

await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
});

test('persists the signed-in session after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('[data-testid="user-id"]')).toHaveText(/^user_/, { timeout: 30_000 });
await expect(electronPage.locator('[data-testid="session-id"]')).toHaveText(/^sess_/, { timeout: 30_000 });
});

test('signs out and clears the session', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-userButtonTrigger')).toBeVisible({ timeout: 30_000 });
await clerk.signOut({ page: electronPage });

await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});

test('keeps the signed-out state after relaunch', async ({ electronPage }) => {
await expect(electronPage.locator('.cl-signIn-root')).toBeVisible({ timeout: 30_000 });
});
});
Loading
Loading