diff --git a/.github/workflows/qa-matrix.yml b/.github/workflows/qa-matrix.yml new file mode 100644 index 0000000..16dfb29 --- /dev/null +++ b/.github/workflows/qa-matrix.yml @@ -0,0 +1,71 @@ +name: QA Matrix + +on: + pull_request: + branches: ['main'] + push: + branches: ['main'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + qa: + name: QA (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 + with: + node-version: 20 + package-manager-cache: false + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Unit and integration tests + run: npm test -- --runInBand + + - name: Build TypeScript + run: npm run build:ts + + - name: Build CSS + run: npm run build:css + + - name: Build renderer bundle + run: npm run build:webpack + + - name: Install Playwright browser (Linux) + if: runner.os == 'Linux' + run: npx playwright install --with-deps chromium + + - name: Install Playwright browser (Windows/macOS) + if: runner.os != 'Linux' + run: npx playwright install chromium + + - name: Capture UI screenshot + run: npm run qa:screenshot + env: + UI_SCREENSHOT_NAME: ui-${{ runner.os }}.png + + - name: Upload UI screenshot + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ui-screenshot-${{ runner.os }} + path: dist/qa/screenshots/*.png + if-no-files-found: error + retention-days: 14 diff --git a/package-lock.json b/package-lock.json index f546c67..7d80b03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.2", + "playwright": "^1.58.2", "postcss": "^8.4.35", "postcss-loader": "^8.1.0", "prettier": "^3.2.5", @@ -15399,6 +15400,53 @@ "node": ">= 0.4.0" } }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 6d82767..6de50b6 100755 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "prepare": "husky install", "sonar": "node scripts/sonar-scan.js", "qa": "node scripts/index.js qa", + "qa:screenshot": "node scripts/capture-ui-screenshot.js", "security": "node scripts/index.js security", "gitleaks": "node scripts/index.js gitleaks", "sbom": "node scripts/index.js sbom", @@ -127,14 +128,14 @@ "@babel/preset-react": "^7.26.3", "@babel/preset-typescript": "^7.26.0", "@electron/rebuild": "^3.6.0", - "@types/jest": "^29.5.14", - "@types/node": "^22.13.4", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", "@jest/globals": "^29.7.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^14.3.1", "@testing-library/user-event": "^14.6.1", + "@types/jest": "^29.5.14", + "@types/node": "^22.13.4", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", "@typescript-eslint/eslint-plugin": "^8.24.0", "@typescript-eslint/parser": "^8.24.0", "autoprefixer": "^10.4.17", @@ -158,12 +159,13 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.2", + "playwright": "^1.58.2", "postcss": "^8.4.35", "postcss-loader": "^8.1.0", "prettier": "^3.2.5", "rimraf": "^5.0.5", - "style-loader": "^3.3.4", "sonarqube-scanner": "^3.3.0", + "style-loader": "^3.3.4", "tailwindcss": "^3.4.1", "typescript": "^5.7.3", "webpack": "^5.90.1", diff --git a/scripts/capture-ui-screenshot.js b/scripts/capture-ui-screenshot.js new file mode 100644 index 0000000..b5f1f8c --- /dev/null +++ b/scripts/capture-ui-screenshot.js @@ -0,0 +1,91 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const http = require('http'); +const path = require('path'); +const { chromium } = require('playwright'); + +const ROOT_DIR = path.join(__dirname, '..'); +const RENDERER_DIR = path.join(ROOT_DIR, 'src', 'renderer'); +const SCREENSHOT_DIR = path.join(ROOT_DIR, 'dist', 'qa', 'screenshots'); +const SCREENSHOT_NAME = + process.env.UI_SCREENSHOT_NAME || `ui-${process.platform}-${process.arch}.png`; +const SCREENSHOT_PATH = path.join(SCREENSHOT_DIR, SCREENSHOT_NAME); +const PORT = Number(process.env.UI_SCREENSHOT_PORT || 4173); + +const MIME_TYPES = { + '.css': 'text/css; charset=UTF-8', + '.html': 'text/html; charset=UTF-8', + '.js': 'application/javascript; charset=UTF-8', + '.json': 'application/json; charset=UTF-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', +}; + +function resolveFilePath(requestUrl) { + const urlPath = decodeURIComponent(requestUrl.split('?')[0]); + const relativePath = urlPath === '/' ? 'index.html' : urlPath.replace(/^\/+/, ''); + const absolutePath = path.resolve(RENDERER_DIR, relativePath); + const relativeToRoot = path.relative(RENDERER_DIR, absolutePath); + + if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) { + return null; + } + + return absolutePath; +} + +function createStaticServer() { + return http.createServer((request, response) => { + const requestedPath = resolveFilePath(request.url || '/'); + + if (!requestedPath) { + response.writeHead(403, { 'Content-Type': 'text/plain; charset=UTF-8' }); + response.end('Forbidden'); + return; + } + + fs.readFile(requestedPath, (error, content) => { + if (error) { + response.writeHead(404, { 'Content-Type': 'text/plain; charset=UTF-8' }); + response.end('Not Found'); + return; + } + + const extension = path.extname(requestedPath).toLowerCase(); + const contentType = MIME_TYPES[extension] || 'application/octet-stream'; + response.writeHead(200, { 'Content-Type': contentType }); + response.end(content); + }); + }); +} + +async function captureScreenshot() { + fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); + + const server = createStaticServer(); + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(PORT, '127.0.0.1', () => resolve()); + }); + + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); + + try { + await page.goto(`http://127.0.0.1:${PORT}/index.html`, { waitUntil: 'networkidle' }); + await page.waitForSelector('#app', { timeout: 10000 }); + await page.waitForTimeout(1000); + await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }); + console.log(`UI screenshot captured: ${SCREENSHOT_PATH}`); + } finally { + await page.close(); + await browser.close(); + await new Promise((resolve) => server.close(resolve)); + } +} + +captureScreenshot().catch((error) => { + console.error(`Failed to capture UI screenshot: ${error.message}`); + process.exit(1); +}); diff --git a/src/main/index.ts b/src/main/index.ts index feba3cd..9225cb6 100755 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -101,6 +101,8 @@ app.on('activate', () => { // IPC Event Handlers +type FilterPatternBundle = string[] & { includePatterns?: string[]; includeExtensions?: string[] }; + // Select directory dialog ipcMain.handle('dialog:selectDirectory', async () => { const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow ?? undefined, { @@ -123,8 +125,7 @@ ipcMain.handle( // in the UI tree view. This is critical for performance with large repositories. // Parse config to get settings and exclude patterns - let excludePatterns: (string[] & { includePatterns?: string[]; includeExtensions?: string[] }) = - ['']; + let excludePatterns: FilterPatternBundle = []; try { const config = (configContent ? (yaml.parse(configContent) as ConfigObject) diff --git a/src/renderer/components/App.tsx b/src/renderer/components/App.tsx index 319b84c..1a0312d 100755 --- a/src/renderer/components/App.tsx +++ b/src/renderer/components/App.tsx @@ -512,9 +512,9 @@ const App = () => { return ( -
+
{/* Tab navigation and content container */} -
+
{/* Tab Bar and title in the same row */}
@@ -578,7 +578,7 @@ const App = () => {
{/* Tab content */} -
+
{activeTab === 'config' && ( )} diff --git a/src/renderer/components/FileTree.tsx b/src/renderer/components/FileTree.tsx index 6cf584b..0be2116 100755 --- a/src/renderer/components/FileTree.tsx +++ b/src/renderer/components/FileTree.tsx @@ -272,7 +272,7 @@ const FileTreeComponent = ({ }; return ( -
+
-
+
{items.length === 0 ? (
                 {processedResult.content}
@@ -211,7 +211,7 @@ const ProcessedTab = ({ processedResult, onSave, onRefresh }: ProcessedTabProps)
               Files by Token Count
             
             
-
+
diff --git a/src/renderer/components/SourceTab.tsx b/src/renderer/components/SourceTab.tsx index 241a55e..7e1877f 100755 --- a/src/renderer/components/SourceTab.tsx +++ b/src/renderer/components/SourceTab.tsx @@ -176,7 +176,7 @@ const SourceTab = ({ }, []); return ( -
+
{directoryTree.length > 0 ? ( -
+