-
Notifications
You must be signed in to change notification settings - Fork 461
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
Changes from all commits
b8b9660
655da71
7dd38a7
6a98c41
4f179e2
File filter
Filter by extension
Conversations
Jump to
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.
|
||
| } | ||
| 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 }); | ||
| }); | ||
| }); |
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