Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 469
test(electron): Initial Electron SDK integration test#9014
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
b8b9660655da717dd38a76a98c414f179e2File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
| Original file line number | Diff line number | Diff 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') | ||
| .addDependency('@clerk/electron', PKGLAB); | ||
| export const electron = { | ||
| vite, | ||
| } as const; | ||
| Original file line number | Diff line number | Diff 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> |
| Original file line number | Diff line number | Diff 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(); | ||
| }); |
| Original file line number | Diff line number | Diff 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" | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| import { exposeClerkBridge } from '@clerk/electron/preload'; | ||
| exposeClerkBridge(); |
| Original file line number | Diff line number | Diff 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
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🛠️ 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
Suggested change
🤖 Prompt for AI AgentsSource: 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>, | ||||||||||||||||||
| ); | ||||||||||||||||||
| Original file line number | Diff line number | Diff 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; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /// <reference types="vite/client" /> |
| Original file line number | Diff line number | Diff 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"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import react from '@vitejs/plugin-react'; | ||
| import { defineConfig } from 'vite'; | ||
| export default defineConfig({ | ||
| plugins: [react()], | ||
| }); |
| Original file line number | Diff line number | Diff 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 }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
devandserveto real commands.pnpm devcurrently just performs a build, andpnpm serveexits immediately, so the generated Electron template has no runnable development or start path.🤖 Prompt for AI Agents