From dee696e7bde4d5e46c84535d08da3f3c164da430 Mon Sep 17 00:00:00 2001 From: Mehdi Date: Tue, 10 Feb 2026 21:29:36 +0000 Subject: [PATCH 1/4] test(e2e): add deep playwright electron process-flow coverage --- .github/workflows/qa-matrix.yml | 15 ++ jest.config.js | 2 +- package.json | 3 + playwright.config.ts | 22 ++ src/main/index.ts | 7 + tests/catalog.md | 8 + tests/e2e/electron-process-flow.spec.ts | 286 ++++++++++++++++++++++++ 7 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 playwright.config.ts create mode 100644 tests/e2e/electron-process-flow.spec.ts diff --git a/.github/workflows/qa-matrix.yml b/.github/workflows/qa-matrix.yml index 16dfb29..d22e163 100644 --- a/.github/workflows/qa-matrix.yml +++ b/.github/workflows/qa-matrix.yml @@ -62,6 +62,10 @@ jobs: env: UI_SCREENSHOT_NAME: ui-${{ runner.os }}.png + - name: Run Electron E2E (Playwright) + if: runner.os == 'Linux' + run: xvfb-run -a npm run e2e:playwright + - name: Upload UI screenshot uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: @@ -69,3 +73,14 @@ jobs: path: dist/qa/screenshots/*.png if-no-files-found: error retention-days: 14 + + - name: Upload Playwright E2E artifacts + if: runner.os == 'Linux' && always() + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: playwright-e2e-linux + path: | + dist/qa/playwright-report + dist/qa/playwright-results + if-no-files-found: warn + retention-days: 14 diff --git a/jest.config.js b/jest.config.js index 38ac370..cc7a293 100755 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,7 @@ module.exports = { '^yaml$': '/tests/mocks/yaml-mock.ts', }, setupFilesAfterEnv: ['/tests/setup.ts'], - testPathIgnorePatterns: ['/node_modules/'], + testPathIgnorePatterns: ['/node_modules/', '/tests/e2e/'], transform: { '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', }, diff --git a/package.json b/package.json index 17caa5b..ba49783 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "qa": "node scripts/index.js qa", "preqa:screenshot": "npm run build:ts", "qa:screenshot": "node scripts/capture-ui-screenshot.js", + "pree2e:playwright": "npm run build:ts && npm run build:css && npm run build:webpack", + "e2e:playwright": "playwright test -c playwright.config.ts", + "e2e:playwright:headed": "playwright test -c playwright.config.ts --headed", "predocs:screenshots": "npm run build:ts && npm run build:webpack", "docs:screenshots": "node scripts/generate-doc-screenshots.js", "security": "node scripts/index.js security", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..fda4406 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + workers: 2, + timeout: 120_000, + expect: { + timeout: 15_000, + }, + reporter: [['list'], ['html', { open: 'never', outputFolder: 'dist/qa/playwright-report' }]], + outputDir: 'dist/qa/playwright-results', + use: { + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + actionTimeout: 15_000, + navigationTimeout: 30_000, + }, +}); diff --git a/src/main/index.ts b/src/main/index.ts index 3a9bd61..a356cb5 100755 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -59,6 +59,13 @@ const createForbiddenAssetResponse = (): Response => new Response('Forbidden', { // Set environment const isDevelopment = process.env.NODE_ENV === 'development'; +const e2eUserDataPath = process.env.ELECTRON_USER_DATA_PATH; + +if (typeof e2eUserDataPath === 'string' && e2eUserDataPath.trim().length > 0) { + const resolvedUserDataPath = path.resolve(e2eUserDataPath); + fs.mkdirSync(resolvedUserDataPath, { recursive: true }); + app.setPath('userData', resolvedUserDataPath); +} async function createWindow() { // Create the browser window diff --git a/tests/catalog.md b/tests/catalog.md index 88bd4d4..cc357c0 100644 --- a/tests/catalog.md +++ b/tests/catalog.md @@ -7,6 +7,7 @@ Purpose: quick map of what is covered, why it exists, and which command to run. - Full tests: `npm test -- --runInBand` - Lint: `npm run lint` - Markdown docs lint (links/images/icons): `npm run lint:md` +- Electron E2E (Playwright): `npm run e2e:playwright` - UI screenshot gate: `npm run qa:screenshot` - Docs screenshots: `npm run docs:screenshots` - Devcontainer smoke: `devcontainer up --workspace-folder .` then `devcontainer exec --workspace-folder . npm run lint` @@ -40,6 +41,12 @@ Purpose: quick map of what is covered, why it exists, and which command to run. | `tests/integration/main-process/xml-export-e2e.test.ts` | XML export pipeline | End-to-end XML shape, CDATA wrapping, invalid-character sanitization, summary metrics | | `tests/integration/pattern-merging.test.ts` | Filtering + gitignore merge behavior | Combined behavior of include/exclude patterns with gitignore toggles | +## Electron E2E Tests + +| File | Primary Target | Key Use Cases | +| ----------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `tests/e2e/electron-process-flow.spec.ts` | Full renderer + preload + main-process wiring | Folder selection, file tree interaction, process flow, XML format handling, refresh-from-disk behavior, save flow | + ## Visual Regression Signal | Command | Primary Target | Key Use Cases | @@ -63,6 +70,7 @@ Purpose: quick map of what is covered, why it exists, and which command to run. - Renderer flow changes: - `tests/unit/components/app.test.tsx` - `tests/unit/components/config-tab.test.tsx` + - `tests/e2e/electron-process-flow.spec.ts` - Main process / IPC changes: - `tests/integration/main-process/handlers.test.ts` - `tests/unit/main/updater.test.ts` diff --git a/tests/e2e/electron-process-flow.spec.ts b/tests/e2e/electron-process-flow.spec.ts new file mode 100644 index 0000000..bd9337d --- /dev/null +++ b/tests/e2e/electron-process-flow.spec.ts @@ -0,0 +1,286 @@ +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { + _electron as electron, + expect, + test as base, + type ElectronApplication, + type Page, +} from 'playwright/test'; + +type E2EFixtures = { + electronApp: ElectronApplication; + page: Page; + projectDir: string; + savePath: string; + userDataDir: string; +}; + +const REPO_ROOT = path.resolve(__dirname, '..', '..'); +const MAIN_ENTRY_PATH = path.join(REPO_ROOT, 'build', 'ts', 'main', 'index.js'); + +const ensureMainEntryExists = () => { + if (!fs.existsSync(MAIN_ENTRY_PATH)) { + throw new Error( + `Missing Electron main entry at ${MAIN_ENTRY_PATH}. Run "npm run build:ts" before Playwright E2E.` + ); + } +}; + +const sanitizeForPath = (value: string): string => + value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') || 'run'; + +const writeFixtureFile = (projectDir: string, relativePath: string, content: string | Buffer) => { + const filePath = path.join(projectDir, relativePath); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + if (typeof content === 'string') { + fs.writeFileSync(filePath, content, 'utf8'); + return; + } + fs.writeFileSync(filePath, content); +}; + +const createFixtureProject = (): string => { + const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-code-fusion-e2e-')); + + writeFixtureFile( + projectDir, + 'src/App.tsx', + [ + "export const APP_MARKER = 'APP_MARKER_V1';", + '', + 'export function App() {', + " return
{APP_MARKER}
;", + '}', + '', + ].join('\n') + ); + writeFixtureFile( + projectDir, + 'src/utils/helper.ts', + [ + "export const HELPER_MARKER = 'HELPER_MARKER_V1';", + '', + 'export const sum = (a: number, b: number) => a + b;', + '', + ].join('\n') + ); + writeFixtureFile(projectDir, 'README.md', '# Fixture Project\n\nUsed for Electron E2E coverage.\n'); + writeFixtureFile(projectDir, '.gitignore', 'dist/\n*.log\n!important.log\n'); + writeFixtureFile(projectDir, '.env', 'LOCAL_SECRET_TOKEN=abc123\n'); + writeFixtureFile(projectDir, 'dist/bundle.js', 'console.log("should be excluded");\n'); + writeFixtureFile(projectDir, 'important.log', 'this file is intentionally present\n'); + writeFixtureFile(projectDir, 'assets/logo.bin', Buffer.from([0, 1, 2, 3, 4, 255])); + + return projectDir; +}; + +const stubNativeDialogs = async ( + electronApp: ElectronApplication, + projectDir: string, + savePath: string +) => { + await electronApp.evaluate( + ({ dialog }, { directoryPath, outputPath }) => { + dialog.showOpenDialog = async () => ({ + canceled: false, + filePaths: [directoryPath], + }); + + dialog.showSaveDialog = async () => ({ + canceled: false, + filePath: outputPath, + }); + }, + { directoryPath: projectDir, outputPath: savePath } + ); +}; + +const configureFlowDefaults = async ( + page: Page, + exportFormat: 'markdown' | 'xml' = 'markdown' +) => { + await page.getByRole('tab', { name: 'Start' }).click(); + await page.getByLabel('Filter by file extensions').uncheck(); + await page.getByLabel('Use exclude patterns').check(); + await page.getByLabel('Apply .gitignore rules').check(); + await page.getByLabel('Scan content for secrets').check(); + await page.getByLabel('Exclude suspicious files').check(); + await page.getByLabel('Include file tree in output').check(); + await page.getByLabel('Display token counts').check(); + await page.getByLabel('Export format').selectOption(exportFormat); + await page.getByRole('button', { name: /save config|saved/i }).click(); + await expect(page.getByLabel('Filter by file extensions')).not.toBeChecked(); + await expect(page.getByLabel('Export format')).toHaveValue(exportFormat); +}; + +const openFixtureProject = async (page: Page, exportFormat: 'markdown' | 'xml' = 'markdown') => { + await configureFlowDefaults(page, exportFormat); + await page.getByRole('button', { name: 'Select Folder' }).click(); + await expect(page.getByRole('tab', { name: 'Select Files' })).toHaveAttribute('aria-selected', 'true'); + await expect(page.getByLabel('Select All')).toBeVisible(); +}; + +const selectSourceFiles = async (page: Page, fileNames: string[]) => { + await page.getByRole('button', { name: /^Expand folder src$/i }).click(); + + const utilsFolderToggle = page.getByRole('button', { name: /^Expand folder utils$/i }); + if ((await utilsFolderToggle.count()) > 0) { + await utilsFolderToggle.first().click(); + } + + for (const fileName of fileNames) { + const fileCheckbox = page.getByRole('checkbox', { name: fileName, exact: true }); + await fileCheckbox.check(); + await expect(fileCheckbox).toBeChecked(); + } + + await expect(page.locator('.file-tree')).toContainText( + new RegExp(`${fileNames.length} of \\d+ files selected`), + { timeout: 15_000 } + ); + + const processButton = page.getByTestId('process-selected-files-button'); + await expect(processButton).toContainText(/process selected files/i, { timeout: 30_000 }); + await expect(processButton).toBeEnabled({ timeout: 30_000 }); + return processButton; +}; + +const processSelection = async (page: Page) => { + const processButton = page.getByTestId('process-selected-files-button'); + await expect(processButton).toBeEnabled({ timeout: 30_000 }); + await processButton.click(); + + await expect(page.getByRole('tab', { name: 'Processed Output' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + return page.locator('#processed-content pre'); +}; + +const test = base.extend({ + projectDir: async ({ browserName }, use) => { + void browserName; + const projectDir = createFixtureProject(); + await use(projectDir); + fs.rmSync(projectDir, { recursive: true, force: true }); + }, + + savePath: async ({ projectDir }, use, testInfo) => { + const savePath = path.join(projectDir, `playwright-${sanitizeForPath(testInfo.title)}.md`); + await use(savePath); + }, + + userDataDir: async ({ browserName }, use) => { + void browserName; + const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-code-fusion-user-data-')); + await use(userDataDir); + fs.rmSync(userDataDir, { recursive: true, force: true }); + }, + + electronApp: async ({ projectDir, savePath, userDataDir }, use) => { + ensureMainEntryExists(); + + const electronApp = await electron.launch({ + args: [MAIN_ENTRY_PATH], + cwd: REPO_ROOT, + env: { + ...process.env, + NODE_ENV: 'test', + ELECTRON_USER_DATA_PATH: userDataDir, + ELECTRON_DISABLE_SECURITY_WARNINGS: 'true', + }, + }); + + await stubNativeDialogs(electronApp, projectDir, savePath); + + await use(electronApp); + await electronApp.close(); + }, + + page: async ({ electronApp }, use) => { + const page = await electronApp.firstWindow(); + await page.waitForLoadState('domcontentloaded'); + await expect(page.getByRole('heading', { name: 'AI Code Fusion' })).toBeVisible(); + await use(page); + }, +}); + +test('processes selected files end-to-end with markdown output', async ({ page }) => { + await openFixtureProject(page); + + await expect(page.getByText('.env', { exact: true })).toHaveCount(0); + await expect(page.getByText('dist', { exact: true })).toHaveCount(0); + + await selectSourceFiles(page, ['App.tsx', 'helper.ts']); + const processedContent = await processSelection(page); + + await expect(processedContent).toContainText('# Repository Content'); + await expect(processedContent).toContainText('src/App.tsx'); + await expect(processedContent).toContainText('src/utils/helper.ts'); + await expect(processedContent).toContainText('APP_MARKER_V1'); + await expect(processedContent).toContainText('HELPER_MARKER_V1'); + + await expect(page.getByRole('cell', { name: 'src/App.tsx' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'src/utils/helper.ts' })).toBeVisible(); +}); + +test('honors XML export format from config during full processing flow', async ({ page }) => { + await openFixtureProject(page, 'xml'); + await selectSourceFiles(page, ['App.tsx']); + const processedContent = await processSelection(page); + + await expect(processedContent).toContainText(''); + await expect(processedContent).toContainText(''); + await expect(processedContent).toContainText(' { + await openFixtureProject(page); + await selectSourceFiles(page, ['App.tsx']); + const processedContent = await processSelection(page); + + await expect(processedContent).toContainText('APP_MARKER_V1'); + + writeFixtureFile( + projectDir, + 'src/App.tsx', + [ + "export const APP_MARKER = 'APP_MARKER_V2';", + '', + 'export function App() {', + " return
{APP_MARKER}
;", + '}', + '', + ].join('\n') + ); + + await page.getByRole('button', { name: 'Refresh Code' }).click(); + await expect(processedContent).toContainText('APP_MARKER_V2'); +}); + +test('saves processed output to disk through the native save flow', async ({ page, savePath }) => { + await openFixtureProject(page); + await selectSourceFiles(page, ['App.tsx']); + await processSelection(page); + + await page.getByRole('button', { name: 'Save to File' }).click(); + + await expect + .poll(() => fs.existsSync(savePath), { + timeout: 15_000, + message: `Expected saved output to exist at ${savePath}`, + }) + .toBe(true); + + const savedContent = fs.readFileSync(savePath, 'utf8'); + expect(savedContent).toContain('src/App.tsx'); + expect(savedContent).toContain('APP_MARKER_V1'); +}); From 0ce5d73394468c4b3776de67ecc3b26df5ed8b66 Mon Sep 17 00:00:00 2001 From: Mehdi Date: Tue, 10 Feb 2026 21:35:10 +0000 Subject: [PATCH 2/4] test(e2e): make electron playwright launch CI-safe on linux --- tests/e2e/electron-process-flow.spec.ts | 28 ++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/e2e/electron-process-flow.spec.ts b/tests/e2e/electron-process-flow.spec.ts index bd9337d..50f8671 100644 --- a/tests/e2e/electron-process-flow.spec.ts +++ b/tests/e2e/electron-process-flow.spec.ts @@ -35,6 +35,15 @@ const sanitizeForPath = (value: string): string => .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') || 'run'; +const getElectronLaunchArgs = (): string[] => { + if (process.platform === 'linux') { + // GitHub Linux runners do not expose setuid chrome-sandbox permissions to workspace files. + return ['--no-sandbox', '--disable-setuid-sandbox', MAIN_ENTRY_PATH]; + } + + return [MAIN_ENTRY_PATH]; +}; + const writeFixtureFile = (projectDir: string, relativePath: string, content: string | Buffer) => { const filePath = path.join(projectDir, relativePath); fs.mkdirSync(path.dirname(filePath), { recursive: true }); @@ -185,16 +194,21 @@ const test = base.extend({ electronApp: async ({ projectDir, savePath, userDataDir }, use) => { ensureMainEntryExists(); + const launchEnv: NodeJS.ProcessEnv = { + ...process.env, + NODE_ENV: 'test', + ELECTRON_USER_DATA_PATH: userDataDir, + ELECTRON_DISABLE_SECURITY_WARNINGS: 'true', + }; + + if (process.platform === 'linux') { + launchEnv.ELECTRON_DISABLE_SANDBOX = 'true'; + } const electronApp = await electron.launch({ - args: [MAIN_ENTRY_PATH], + args: getElectronLaunchArgs(), cwd: REPO_ROOT, - env: { - ...process.env, - NODE_ENV: 'test', - ELECTRON_USER_DATA_PATH: userDataDir, - ELECTRON_DISABLE_SECURITY_WARNINGS: 'true', - }, + env: launchEnv, }); await stubNativeDialogs(electronApp, projectDir, savePath); From 4314a66246c18f991522ad54362779cc70c8d1f8 Mon Sep 17 00:00:00 2001 From: Mehdi Date: Tue, 10 Feb 2026 21:40:52 +0000 Subject: [PATCH 3/4] fix(e2e): address review feedback and CI robustness gaps --- .github/workflows/qa-matrix.yml | 4 +++- package.json | 1 + src/main/index.ts | 23 ++++++++++++++++++++--- tests/e2e/electron-process-flow.spec.ts | 2 +- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/qa-matrix.yml b/.github/workflows/qa-matrix.yml index d22e163..0245f8e 100644 --- a/.github/workflows/qa-matrix.yml +++ b/.github/workflows/qa-matrix.yml @@ -58,15 +58,17 @@ jobs: run: npx playwright install chromium - name: Capture UI screenshot + id: capture_ui_screenshot run: npm run qa:screenshot env: UI_SCREENSHOT_NAME: ui-${{ runner.os }}.png - name: Run Electron E2E (Playwright) if: runner.os == 'Linux' - run: xvfb-run -a npm run e2e:playwright + run: xvfb-run -a npm run e2e:playwright:ci - name: Upload UI screenshot + if: always() && steps.capture_ui_screenshot.outcome == 'success' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: name: ui-screenshot-${{ runner.os }} diff --git a/package.json b/package.json index ba49783..2f721f9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "qa:screenshot": "node scripts/capture-ui-screenshot.js", "pree2e:playwright": "npm run build:ts && npm run build:css && npm run build:webpack", "e2e:playwright": "playwright test -c playwright.config.ts", + "e2e:playwright:ci": "playwright test -c playwright.config.ts", "e2e:playwright:headed": "playwright test -c playwright.config.ts --headed", "predocs:screenshots": "npm run build:ts && npm run build:webpack", "docs:screenshots": "node scripts/generate-doc-screenshots.js", diff --git a/src/main/index.ts b/src/main/index.ts index a356cb5..120ad35 100755 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,6 +1,7 @@ import { app, BrowserWindow, dialog, ipcMain, net, protocol } from 'electron'; import { autoUpdater } from 'electron-updater'; import fs from 'fs'; +import os from 'os'; import path from 'path'; import { pathToFileURL } from 'node:url'; import yaml from 'yaml'; @@ -60,11 +61,27 @@ const createForbiddenAssetResponse = (): Response => new Response('Forbidden', { // Set environment const isDevelopment = process.env.NODE_ENV === 'development'; const e2eUserDataPath = process.env.ELECTRON_USER_DATA_PATH; +const isPathWithinTempRoot = (candidatePath: string): boolean => { + const tempRootPath = path.resolve(os.tmpdir()); + const resolvedCandidatePath = path.resolve(candidatePath); + const relativePath = path.relative(tempRootPath, resolvedCandidatePath); + + return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)); +}; -if (typeof e2eUserDataPath === 'string' && e2eUserDataPath.trim().length > 0) { +if ( + process.env.NODE_ENV === 'test' && + typeof e2eUserDataPath === 'string' && + e2eUserDataPath.trim().length > 0 +) { const resolvedUserDataPath = path.resolve(e2eUserDataPath); - fs.mkdirSync(resolvedUserDataPath, { recursive: true }); - app.setPath('userData', resolvedUserDataPath); + + if (isPathWithinTempRoot(resolvedUserDataPath)) { + fs.mkdirSync(resolvedUserDataPath, { recursive: true }); + app.setPath('userData', resolvedUserDataPath); + } else { + console.warn(`Ignoring ELECTRON_USER_DATA_PATH outside temp root: ${resolvedUserDataPath}`); + } } async function createWindow() { diff --git a/tests/e2e/electron-process-flow.spec.ts b/tests/e2e/electron-process-flow.spec.ts index 50f8671..4e0d61d 100644 --- a/tests/e2e/electron-process-flow.spec.ts +++ b/tests/e2e/electron-process-flow.spec.ts @@ -81,7 +81,7 @@ const createFixtureProject = (): string => { ); writeFixtureFile(projectDir, 'README.md', '# Fixture Project\n\nUsed for Electron E2E coverage.\n'); writeFixtureFile(projectDir, '.gitignore', 'dist/\n*.log\n!important.log\n'); - writeFixtureFile(projectDir, '.env', 'LOCAL_SECRET_TOKEN=abc123\n'); + writeFixtureFile(projectDir, '.env', 'LOCAL_TEST_VALUE=fixture\n'); writeFixtureFile(projectDir, 'dist/bundle.js', 'console.log("should be excluded");\n'); writeFixtureFile(projectDir, 'important.log', 'this file is intentionally present\n'); writeFixtureFile(projectDir, 'assets/logo.bin', Buffer.from([0, 1, 2, 3, 4, 255])); From 214f57f7cc44a7e214fd395740206b69d6bb3cf6 Mon Sep 17 00:00:00 2001 From: Mehdi Date: Tue, 10 Feb 2026 21:48:13 +0000 Subject: [PATCH 4/4] fix(sonar): resolve new-code reliability and hotspot findings --- src/main/index.ts | 2 +- tests/e2e/electron-process-flow.spec.ts | 57 ++++++++++++++++++------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 120ad35..5721ad8 100755 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,7 +1,7 @@ import { app, BrowserWindow, dialog, ipcMain, net, protocol } from 'electron'; import { autoUpdater } from 'electron-updater'; import fs from 'fs'; -import os from 'os'; +import os from 'node:os'; import path from 'path'; import { pathToFileURL } from 'node:url'; import yaml from 'yaml'; diff --git a/tests/e2e/electron-process-flow.spec.ts b/tests/e2e/electron-process-flow.spec.ts index 4e0d61d..70678f7 100644 --- a/tests/e2e/electron-process-flow.spec.ts +++ b/tests/e2e/electron-process-flow.spec.ts @@ -1,6 +1,6 @@ -import fs from 'fs'; -import os from 'os'; -import path from 'path'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import { _electron as electron, expect, @@ -28,12 +28,37 @@ const ensureMainEntryExists = () => { } }; -const sanitizeForPath = (value: string): string => - value - .trim() - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') || 'run'; +const sanitizeForPath = (value: string): string => { + const normalizedValue = value.trim().toLowerCase(); + let output = ''; + let lastCharWasDash = false; + + for (const character of normalizedValue) { + const isLowerCaseLetter = character >= 'a' && character <= 'z'; + const isNumber = character >= '0' && character <= '9'; + + if (isLowerCaseLetter || isNumber) { + output += character; + lastCharWasDash = false; + continue; + } + + if (!lastCharWasDash) { + output += '-'; + lastCharWasDash = true; + } + } + + while (output.startsWith('-')) { + output = output.slice(1); + } + + while (output.endsWith('-')) { + output = output.slice(0, -1); + } + + return output || 'run'; +}; const getElectronLaunchArgs = (): string[] => { if (process.platform === 'linux') { @@ -54,8 +79,8 @@ const writeFixtureFile = (projectDir: string, relativePath: string, content: str fs.writeFileSync(filePath, content); }; -const createFixtureProject = (): string => { - const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-code-fusion-e2e-')); +const createFixtureProject = (browserName: string): string => { + const projectDir = fs.mkdtempSync(path.join(os.tmpdir(), `ai-code-fusion-e2e-${browserName}-`)); writeFixtureFile( projectDir, @@ -150,7 +175,7 @@ const selectSourceFiles = async (page: Page, fileNames: string[]) => { } await expect(page.locator('.file-tree')).toContainText( - new RegExp(`${fileNames.length} of \\d+ files selected`), + new RegExp(String.raw`${fileNames.length} of \d+ files selected`), { timeout: 15_000 } ); @@ -174,8 +199,7 @@ const processSelection = async (page: Page) => { const test = base.extend({ projectDir: async ({ browserName }, use) => { - void browserName; - const projectDir = createFixtureProject(); + const projectDir = createFixtureProject(browserName); await use(projectDir); fs.rmSync(projectDir, { recursive: true, force: true }); }, @@ -186,8 +210,9 @@ const test = base.extend({ }, userDataDir: async ({ browserName }, use) => { - void browserName; - const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-code-fusion-user-data-')); + const userDataDir = fs.mkdtempSync( + path.join(os.tmpdir(), `ai-code-fusion-user-data-${browserName}-`) + ); await use(userDataDir); fs.rmSync(userDataDir, { recursive: true, force: true }); },