diff --git a/Makefile b/Makefile index c6cd6b2..d5a2dfe 100755 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ # Make these targets phony (they don't create files with these names) .PHONY: all setup dev clean clean-all build build-win build-linux \ build-mac build-mac-arm build-mac-universal \ - test css css-watch lint format validate qa setup-hooks sonar \ + test css css-watch lint lint-md format validate qa docs-screenshots setup-hooks sonar \ security gitleaks sbom renovate renovate-local mend-scan \ icons sample-logo release @@ -59,6 +59,9 @@ css-watch: setup-scripts lint: setup-scripts @node scripts/index.js lint +lint-md: setup-scripts + @node scripts/index.js lint-md + format: setup-scripts @node scripts/index.js format @@ -68,6 +71,9 @@ validate: setup-scripts qa: setup-scripts @node scripts/index.js qa +docs-screenshots: setup-scripts + @node scripts/index.js docs-screenshots + setup-hooks: setup-scripts @node scripts/index.js hooks diff --git a/README.md b/README.md index 43fa5e3..7bc5011 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,47 @@ A desktop app to prepare code repositories for AI workflows. - File filtering with custom patterns and `.gitignore` support - Token counting support for selected files - Processed output ready to copy/export for AI tools +- Export format selector: Markdown or XML - Cross-platform support (Windows, macOS, Linux) +- UI panel screenshots: `docs/APP_VIEWS.md` + +## Processed Output Example + +![Processed Output panel](docs/images/app-processed-panel.png) + +Full sample files: + +- Markdown: [`docs/examples/output-markdown.md`](docs/examples/output-markdown.md) +- XML: [`docs/examples/output.xml`](docs/examples/output.xml) + +### Markdown export example + +````md +# Repository Analysis + +## src/App.tsx + +```ts +export function App() { + return
Hello AI Code Fusion
; +} +``` + +Tokens: 120 +```` + +### XML export example + +```xml + + + Hello AI Code Fusion; +} + ]]> + +``` ## Download Release diff --git a/assets/ai_code_fusion_1.jpg b/assets/ai_code_fusion_1.jpg deleted file mode 100755 index d27d5a0..0000000 Binary files a/assets/ai_code_fusion_1.jpg and /dev/null differ diff --git a/assets/ai_code_fusion_2.jpg b/assets/ai_code_fusion_2.jpg deleted file mode 100755 index c5029da..0000000 Binary files a/assets/ai_code_fusion_2.jpg and /dev/null differ diff --git a/assets/ai_code_fusion_3.jpg b/assets/ai_code_fusion_3.jpg deleted file mode 100755 index 74bccc9..0000000 Binary files a/assets/ai_code_fusion_3.jpg and /dev/null differ diff --git a/assets/ai_code_fusion_4.jpg b/assets/ai_code_fusion_4.jpg deleted file mode 100755 index 41fb1f5..0000000 Binary files a/assets/ai_code_fusion_4.jpg and /dev/null differ diff --git a/docs/APP_VIEWS.md b/docs/APP_VIEWS.md new file mode 100644 index 0000000..d53fdbf --- /dev/null +++ b/docs/APP_VIEWS.md @@ -0,0 +1,31 @@ +# App Views + +This page shows up-to-date screenshots for the main app panels. + +## Start Panel (Config) + +![Start panel with filtering and output options](images/app-config-panel.png) + +## Select Files Panel + +![Select Files panel with repository tree](images/app-select-panel.png) + +## Select Files Panel (With Selection) + +![Select Files panel after choosing files](images/app-select-panel-selected.png) + +## Select Files Panel (Resized) + +![Select Files panel in resized viewport](images/app-select-panel-resized.png) + +## Processed Output Panel + +![Processed Output panel with generated content and token table](images/app-processed-panel.png) + +## Refresh Screenshots + +```bash +npm run docs:screenshots +``` + +This command runs the Playwright capture flow and updates screenshots in `docs/images/`. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a808b2d..934d93b 100755 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -46,9 +46,11 @@ make build-mac # Quality make test make lint +make lint-md make format make validate make qa +make docs-screenshots # Security / dependency automation make security diff --git a/docs/examples/output-markdown.md b/docs/examples/output-markdown.md new file mode 100644 index 0000000..1971ed8 --- /dev/null +++ b/docs/examples/output-markdown.md @@ -0,0 +1,23 @@ +# Repository Analysis + +## src/App.tsx + +```ts +export function App() { + return
Hello AI Code Fusion
; +} +``` + +Tokens: 120 + +## src/features/feature-24/ui/Feature24Panel.tsx + +```ts +export function Feature24Panel() { + return
Panel content
; +} +``` + +Tokens: 240 + +--END-- diff --git a/docs/examples/output.xml b/docs/examples/output.xml new file mode 100644 index 0000000..5dce688 --- /dev/null +++ b/docs/examples/output.xml @@ -0,0 +1,13 @@ + + + Hello AI Code Fusion; +} + ]]> + Panel content; +} + ]]> + diff --git a/docs/images/app-config-panel.png b/docs/images/app-config-panel.png new file mode 100644 index 0000000..6b37786 Binary files /dev/null and b/docs/images/app-config-panel.png differ diff --git a/docs/images/app-processed-panel.png b/docs/images/app-processed-panel.png new file mode 100644 index 0000000..b1d72d9 Binary files /dev/null and b/docs/images/app-processed-panel.png differ diff --git a/docs/images/app-select-panel-resized.png b/docs/images/app-select-panel-resized.png new file mode 100644 index 0000000..a045102 Binary files /dev/null and b/docs/images/app-select-panel-resized.png differ diff --git a/docs/images/app-select-panel-selected.png b/docs/images/app-select-panel-selected.png new file mode 100644 index 0000000..7b6be90 Binary files /dev/null and b/docs/images/app-select-panel-selected.png differ diff --git a/docs/images/app-select-panel.png b/docs/images/app-select-panel.png new file mode 100644 index 0000000..dcc12f2 Binary files /dev/null and b/docs/images/app-select-panel.png differ diff --git a/package.json b/package.json index 12f4110..28ac626 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "predev": "npm run build:ts && node scripts/clean-dev-assets.js", "dev": "node scripts/index.js dev", "clear-assets": "rimraf src/renderer/bundle.js src/renderer/bundle.js.map src/renderer/bundle.js.LICENSE.txt src/renderer/output.css", - "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint src tests --ext .js,.jsx,.ts,.tsx --cache", + "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint src tests --ext .js,.jsx,.ts,.tsx --cache && npm run lint:md", + "lint:md": "node scripts/lint-markdown-links.js", "lint:tests": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint tests --ext .js,.jsx,.ts,.tsx --cache", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,html,css}\"", "test": "jest --config jest.config.js --passWithNoTests", @@ -33,6 +34,8 @@ "qa": "node scripts/index.js qa", "preqa:screenshot": "npm run build:ts", "qa:screenshot": "node scripts/capture-ui-screenshot.js", + "predocs:screenshots": "npm run build:ts && npm run build:webpack", + "docs:screenshots": "node scripts/generate-doc-screenshots.js", "security": "node scripts/index.js security", "gitleaks": "node scripts/index.js gitleaks", "gitleaks:staged": "node scripts/index.js gitleaks-staged", diff --git a/scripts/capture-ui-screenshot.js b/scripts/capture-ui-screenshot.js index 875e302..7b967ab 100644 --- a/scripts/capture-ui-screenshot.js +++ b/scripts/capture-ui-screenshot.js @@ -7,7 +7,8 @@ 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 DEFAULT_SCREENSHOT_DIR = path.join('dist', 'qa', 'screenshots'); +const SCREENSHOT_DIR = resolveOutputDirectory(process.env.UI_SCREENSHOT_DIR); const PORT = Number(process.env.UI_SCREENSHOT_PORT || 4173); const DEFAULT_SCREENSHOT_NAME = `ui-${process.platform}-${process.arch}.png`; const FIXED_MTIME = 1700000000000; @@ -58,6 +59,21 @@ function sanitizeScreenshotName(nameCandidate) { return withExtension; } +function resolveOutputDirectory(dirCandidate) { + const rawDir = + typeof dirCandidate === 'string' && dirCandidate.trim() + ? dirCandidate.trim() + : DEFAULT_SCREENSHOT_DIR; + const absoluteDir = path.resolve(ROOT_DIR, rawDir); + const relativeToRoot = path.relative(ROOT_DIR, absoluteDir); + + if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) { + throw new Error(`Invalid screenshot directory: ${rawDir}`); + } + + return absoluteDir; +} + function resolveOutputPath(fileName) { const targetPath = path.resolve(SCREENSHOT_DIR, fileName); const relativeToRoot = path.relative(SCREENSHOT_DIR, targetPath); @@ -313,12 +329,14 @@ const SCREENSHOTS = { sourceTab: resolveOutputPath(`${SCREENSHOT_BASE_NAME}-source.png`), sourceSelected: resolveOutputPath(`${SCREENSHOT_BASE_NAME}-source-selected.png`), sourceSelectedResized: resolveOutputPath(`${SCREENSHOT_BASE_NAME}-source-selected-resized.png`), + processedTab: resolveOutputPath(`${SCREENSHOT_BASE_NAME}-processed.png`), }; const UI_SELECTORS = { appRoot: '#app', configTab: '[data-tab="config"]', sourceTab: '[data-tab="source"]', + processedTabActive: '[data-tab="processed"][aria-selected="true"]', secretScanningToggle: '#enable-secret-scanning', suspiciousFilesToggle: '#exclude-suspicious-files', sourceFolderExpandButton: 'button[aria-label="Expand folder src"]', @@ -331,6 +349,7 @@ const UI_SELECTORS = { refreshFileListButton: 'button[title="Refresh the file list"]', fileTreeScrollContainer: '.file-tree .overflow-auto', processSelectedFilesButton: '[data-testid="process-selected-files-button"]', + processedContent: '#processed-content', }; async function setupMockElectronApi(page) { @@ -358,20 +377,65 @@ async function setupMockElectronApi(page) { const tree = excludeSensitiveFiles ? mockFilteredDirectoryTree : mockDirectoryTree; return cloneTree(tree); }, - analyzeRepository: async () => ({ - totalFiles: 0, - totalTokens: 0, - files: [], - }), - processRepository: async () => ({ - content: '', - stats: { - totalFiles: 0, - totalTokens: 0, + analyzeRepository: async (options = {}) => { + const selectedFilePaths = Array.isArray(options?.selectedFiles) ? options.selectedFiles : []; + const filesInfo = selectedFilePaths.map((filePath, index) => { + const normalizedPath = String(filePath); + const relativePath = normalizedPath.startsWith(`${mockRootPath}/`) + ? normalizedPath.slice(mockRootPath.length + 1) + : normalizedPath; + return { + path: relativePath, + tokens: 120 * (index + 1), + isBinary: false, + }; + }); + + return { + totalFiles: filesInfo.length, + totalTokens: filesInfo.reduce((sum, file) => sum + file.tokens, 0), + filesInfo, + }; + }, + processRepository: async (options = {}) => { + const inputFilesInfo = Array.isArray(options?.filesInfo) ? options.filesInfo : []; + const filesInfo = inputFilesInfo.map((file, index) => ({ + path: String(file?.path || `src/file-${index + 1}.ts`), + tokens: + Number.isFinite(file?.tokens) && Number(file.tokens) > 0 ? Number(file.tokens) : 120 * (index + 1), + isBinary: false, + })); + const totalTokens = filesInfo.reduce((sum, file) => sum + file.tokens, 0); + const exportFormat = options?.options?.exportFormat === 'xml' ? 'xml' : 'markdown'; + const content = + exportFormat === 'xml' + ? [ + '', + ``, + ...filesInfo.map( + (file) => + ` ` + ), + '', + ].join('\n') + : [ + '# Repository Analysis', + '', + ...filesInfo.map( + (file) => `## ${file.path}\n\n\`\`\`ts\n// Preview for ${file.path}\n\`\`\`\nTokens: ${file.tokens}\n` + ), + '--END--', + ].join('\n'); + + return { + content, + exportFormat, + totalTokens, + processedFiles: filesInfo.length, skippedFiles: 0, - processedFiles: 0, - }, - }), + filesInfo, + }; + }, countFilesTokens: async (options) => { const filePaths = Array.isArray(options?.filePaths) ? options.filePaths : []; const results = {}; @@ -569,6 +633,33 @@ async function captureAppStateScreenshots(page) { await runStep('Capture resized screenshot with deep tree expanded', async () => { await page.screenshot({ path: SCREENSHOTS.sourceSelectedResized, fullPage: true }); }); + + await runStep('Return to desktop viewport before processing', async () => { + await page.setViewportSize({ width: 1440, height: 900 }); + }); + + await runStep('Wait for process button to be enabled', async () => { + await page.waitForFunction((selector) => { + const button = document.querySelector(selector); + if (!(button instanceof HTMLButtonElement)) { + return false; + } + return !button.disabled && /process selected files/i.test(button.textContent || ''); + }, UI_SELECTORS.processSelectedFilesButton); + }); + + await runStep('Process selected files', async () => { + await page.locator(UI_SELECTORS.processSelectedFilesButton).first().click(); + }); + + await runStep('Wait for processed panel to render', async () => { + await page.waitForSelector(UI_SELECTORS.processedTabActive, { timeout: 10000 }); + await page.waitForSelector(UI_SELECTORS.processedContent, { timeout: 10000 }); + }); + + await runStep('Capture processed tab screenshot', async () => { + await page.screenshot({ path: SCREENSHOTS.processedTab, fullPage: true }); + }); } async function captureScreenshot() { diff --git a/scripts/generate-doc-screenshots.js b/scripts/generate-doc-screenshots.js new file mode 100755 index 0000000..07b4221 --- /dev/null +++ b/scripts/generate-doc-screenshots.js @@ -0,0 +1,62 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const ROOT_DIR = path.join(__dirname, '..'); +const CAPTURE_SCRIPT_PATH = path.join(__dirname, 'capture-ui-screenshot.js'); +const TEMP_SCREENSHOT_DIR = path.join(ROOT_DIR, 'dist', 'docs', 'screenshots'); +const DOCS_IMAGE_DIR = path.join(ROOT_DIR, 'docs', 'images'); +const TEMP_BASE_NAME = 'docs-panels'; + +const screenshotMap = [ + { from: `${TEMP_BASE_NAME}.png`, to: 'app-config-panel.png' }, + { from: `${TEMP_BASE_NAME}-source.png`, to: 'app-select-panel.png' }, + { from: `${TEMP_BASE_NAME}-source-selected.png`, to: 'app-select-panel-selected.png' }, + { from: `${TEMP_BASE_NAME}-source-selected-resized.png`, to: 'app-select-panel-resized.png' }, + { from: `${TEMP_BASE_NAME}-processed.png`, to: 'app-processed-panel.png' }, +]; + +function fail(message) { + console.error(`Failed to generate docs screenshots: ${message}`); + process.exit(1); +} + +function runCaptureScript() { + const captureEnv = { + ...process.env, + UI_SCREENSHOT_DIR: path.relative(ROOT_DIR, TEMP_SCREENSHOT_DIR), + UI_SCREENSHOT_NAME: `${TEMP_BASE_NAME}.png`, + UI_SCREENSHOT_PORT: process.env.UI_SCREENSHOT_PORT || '4174', + }; + + const result = spawnSync(process.execPath, [CAPTURE_SCRIPT_PATH], { + cwd: ROOT_DIR, + env: captureEnv, + stdio: 'inherit', + }); + + if (result.status !== 0) { + fail(`capture script exited with code ${result.status}`); + } +} + +function copyScreenshotsToDocs() { + fs.mkdirSync(DOCS_IMAGE_DIR, { recursive: true }); + + for (const { from, to } of screenshotMap) { + const sourcePath = path.join(TEMP_SCREENSHOT_DIR, from); + const targetPath = path.join(DOCS_IMAGE_DIR, to); + + if (!fs.existsSync(sourcePath)) { + fail(`missing screenshot ${sourcePath}`); + } + + fs.copyFileSync(sourcePath, targetPath); + console.log(`Updated docs screenshot: ${targetPath}`); + } +} + +runCaptureScript(); +copyScreenshotsToDocs(); diff --git a/scripts/index.js b/scripts/index.js index 7dca026..b1a8cfd 100755 --- a/scripts/index.js +++ b/scripts/index.js @@ -114,6 +114,12 @@ async function executeCommand() { console.log('Linting completed successfully'); break; + case 'lint:md': + case 'lint-md': + await utils.runNpmScript('lint:md'); + console.log('Markdown linting completed successfully'); + break; + case 'format': await utils.runNpmScript('format'); console.log('Formatting completed successfully'); @@ -134,6 +140,12 @@ async function executeCommand() { console.log('QA checks completed successfully'); break; + case 'docs-screenshots': + case 'docs:screenshots': + await utils.runNpmScript('docs:screenshots'); + console.log('Docs screenshots refreshed successfully'); + break; + // Security automation commands case 'security': await security.runSecurity(); diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index d937a92..e318108 100755 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -226,9 +226,11 @@ function printHelp() { console.log(' test - Run tests'); console.log(' test:watch - Run tests in watch mode'); console.log(' lint - Run linter'); + console.log(' lint:md - Validate markdown links, image paths, and no decorative icons'); console.log(' format - Format code'); console.log(' validate - Run all code quality checks'); console.log(' qa - Run lint + tests + security checks'); + console.log(' docs-screenshots - Refresh docs UI screenshots'); console.log(' security - Run security checks (gitleaks + sbom)'); console.log(' gitleaks - Run gitleaks secret scan'); console.log(' sbom - Generate CycloneDX SBOM'); diff --git a/scripts/lint-markdown-links.js b/scripts/lint-markdown-links.js new file mode 100755 index 0000000..52dedc6 --- /dev/null +++ b/scripts/lint-markdown-links.js @@ -0,0 +1,195 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const ROOT_DIR = path.join(__dirname, '..'); + +const IGNORED_PROTOCOLS = ['http://', 'https://', 'mailto:', 'tel:', 'data:', 'javascript:']; +const DECORATIVE_ICON_PATTERN = /\p{Extended_Pictographic}/u; + +function ensureError(error) { + if (error instanceof Error) { + return error; + } + + return new Error(String(error)); +} + +function getMarkdownFiles() { + try { + const output = execSync('git ls-files "*.md"', { + cwd: ROOT_DIR, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + + return output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .map((filePath) => path.join(ROOT_DIR, filePath)); + } catch (error) { + throw new Error(`Unable to list markdown files: ${ensureError(error).message}`); + } +} + +function isExternalTarget(target) { + const normalizedTarget = target.toLowerCase(); + return IGNORED_PROTOCOLS.some((protocol) => normalizedTarget.startsWith(protocol)); +} + +function normalizeTarget(rawTarget) { + if (!rawTarget) { + return ''; + } + + let target = rawTarget.trim(); + + if (target.startsWith('<') && target.endsWith('>')) { + target = target.slice(1, -1).trim(); + } + + const titleSuffixMatch = target.match(/^([^\s]+)\s+["'(].*$/); + if (titleSuffixMatch) { + target = titleSuffixMatch[1]; + } + + return target; +} + +function resolveTargetPath(markdownFilePath, target) { + const [pathWithoutAnchor] = target.split('#'); + + if (!pathWithoutAnchor || isExternalTarget(pathWithoutAnchor) || pathWithoutAnchor.startsWith('#')) { + return null; + } + + if (path.isAbsolute(pathWithoutAnchor)) { + return path.resolve(ROOT_DIR, `.${pathWithoutAnchor}`); + } + + return path.resolve(path.dirname(markdownFilePath), pathWithoutAnchor); +} + +function extractTargetsFromLine(line) { + const targets = []; + + const markdownLinkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g; + let linkMatch; + while ((linkMatch = markdownLinkPattern.exec(line)) !== null) { + targets.push(linkMatch[1]); + } + + const htmlImagePattern = /]*\bsrc=["']([^"']+)["'][^>]*>/gi; + let imageMatch; + while ((imageMatch = htmlImagePattern.exec(line)) !== null) { + targets.push(imageMatch[1]); + } + + return targets; +} + +function lintMarkdownFile(markdownFilePath) { + const content = fs.readFileSync(markdownFilePath, 'utf8'); + const lines = content.split(/\r?\n/); + const errors = []; + + let inFenceBlock = false; + let fenceMarker = ''; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const fenceMatch = line.match(/^\s*(```|~~~)/); + + if (fenceMatch) { + const currentFenceMarker = fenceMatch[1]; + if (!inFenceBlock) { + inFenceBlock = true; + fenceMarker = currentFenceMarker; + } else if (currentFenceMarker === fenceMarker) { + inFenceBlock = false; + fenceMarker = ''; + } + continue; + } + + if (inFenceBlock) { + continue; + } + + if (DECORATIVE_ICON_PATTERN.test(line)) { + errors.push({ + kind: 'decorative-icon', + filePath: markdownFilePath, + lineNumber: index + 1, + lineText: line.trim(), + }); + } + + const rawTargets = extractTargetsFromLine(line); + for (const rawTarget of rawTargets) { + const normalizedTarget = normalizeTarget(rawTarget); + if (!normalizedTarget || isExternalTarget(normalizedTarget) || normalizedTarget.startsWith('#')) { + continue; + } + + const resolvedTargetPath = resolveTargetPath(markdownFilePath, normalizedTarget); + if (!resolvedTargetPath) { + continue; + } + + if (!fs.existsSync(resolvedTargetPath)) { + errors.push({ + kind: 'missing-target', + filePath: markdownFilePath, + lineNumber: index + 1, + target: normalizedTarget, + resolvedPath: resolvedTargetPath, + }); + } + } + } + + return errors; +} + +function run() { + const markdownFiles = getMarkdownFiles(); + let linkCount = 0; + const allErrors = []; + + for (const markdownFilePath of markdownFiles) { + const fileErrors = lintMarkdownFile(markdownFilePath); + allErrors.push(...fileErrors); + + const content = fs.readFileSync(markdownFilePath, 'utf8'); + const lines = content.split(/\r?\n/); + for (const line of lines) { + linkCount += extractTargetsFromLine(line).length; + } + } + + if (allErrors.length > 0) { + console.error('Markdown docs lint failed:\n'); + for (const error of allErrors) { + const relativeFilePath = path.relative(ROOT_DIR, error.filePath); + + if (error.kind === 'decorative-icon') { + console.error(`- ${relativeFilePath}:${error.lineNumber} -> decorative icon found: ${error.lineText}`); + continue; + } + + const relativeResolvedPath = path.relative(ROOT_DIR, error.resolvedPath); + console.error(`- ${relativeFilePath}:${error.lineNumber} -> ${error.target} (missing: ${relativeResolvedPath})`); + } + process.exit(1); + } + + console.log( + `Markdown docs lint passed: ${markdownFiles.length} markdown files checked, ${linkCount} links/images scanned, no decorative icons found.` + ); +} + +run(); diff --git a/tests/catalog.md b/tests/catalog.md index cee4a83..e513c6b 100644 --- a/tests/catalog.md +++ b/tests/catalog.md @@ -6,7 +6,9 @@ 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` - UI screenshot gate: `npm run qa:screenshot` +- Docs screenshots: `npm run docs:screenshots` ## Unit Tests @@ -37,9 +39,14 @@ Purpose: quick map of what is covered, why it exists, and which command to run. ## Visual Regression Signal -| Command | Primary Target | Key Use Cases | -| ----------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | -| `npm run qa:screenshot` | `scripts/capture-ui-screenshot.js` + renderer UI | Cross-OS UI sanity, resized layout checks, deep file-tree selection visibility, secret-filter toggle behavior | +| Command | Primary Target | Key Use Cases | +| -------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `npm run qa:screenshot` | `scripts/capture-ui-screenshot.js` + renderer UI | Cross-OS UI sanity, resized layout checks, deep file-tree selection visibility, secret-filter toggle behavior | +| `npm run docs:screenshots` | `scripts/generate-doc-screenshots.js` + renderer UI | Refresh tracked screenshots for Config/Select/Processed panels in `docs/APP_VIEWS.md` | + +## Manual UI Doc Test + +- `tests/manual/docs-ui-screenshots.md` ## Change-to-Test Mapping diff --git a/tests/manual/docs-ui-screenshots.md b/tests/manual/docs-ui-screenshots.md new file mode 100644 index 0000000..484ea18 --- /dev/null +++ b/tests/manual/docs-ui-screenshots.md @@ -0,0 +1,29 @@ +# Manual Test: Docs UI Screenshots + +Purpose: regenerate and verify documentation screenshots for the main app views. + +## Command + +```bash +npm run docs:screenshots +``` + +## Expected Outputs + +The command should update these files: + +- `docs/images/app-config-panel.png` +- `docs/images/app-select-panel.png` +- `docs/images/app-select-panel-selected.png` +- `docs/images/app-select-panel-resized.png` +- `docs/images/app-processed-panel.png` + +## Verification Checklist + +1. Open `docs/APP_VIEWS.md`. +2. Confirm all five images render. +3. Confirm screenshots reflect current UI labels: + - `Start` + - `Select Files` + - `Processed Output` +4. Confirm `Processed Output` screenshot shows content and file/token summary.