From 3695831859f431f415f591e19d17abd5dab0f6a4 Mon Sep 17 00:00:00 2001 From: DNGriffin Date: Sat, 31 Jan 2026 21:49:41 -0600 Subject: [PATCH 01/16] add react component support --- package-lock.json | 39 +++-- package.json | 1 + public/manifest.json | 8 +- src/background/MessageRouter.ts | 19 +++ src/content/index.ts | 66 +++++++- src/content/react-extractor.ts | 273 +++++++++++++++++++++++++++++++ src/exporter/MarkdownExporter.ts | 52 ++++++ src/prompts/templates.ts | 24 +++ src/shared/types.ts | 10 ++ vite.config.ts | 33 +++- 10 files changed, 505 insertions(+), 20 deletions(-) create mode 100644 src/content/react-extractor.ts diff --git a/package-lock.json b/package-lock.json index e0fb78a..be64b22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "clankercontext", - "version": "1.1.6", + "version": "1.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "clankercontext", - "version": "1.1.6", + "version": "1.1.7", "dependencies": { "@tailwindcss/vite": "^4.1.18", + "bippy": "^0.5.28", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", @@ -57,7 +58,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1270,15 +1270,12 @@ "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" }, "node_modules/@types/react": { "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", - "dev": true, - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1293,6 +1290,15 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", + "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1358,6 +1364,18 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bippy": { + "version": "0.5.28", + "resolved": "https://registry.npmjs.org/bippy/-/bippy-0.5.28.tgz", + "integrity": "sha512-n3UosYu2KoCQDeAw/zSyjoJksT2viBwGhmEgIdCcGXUQgQsmSJc1Hr7s9eneawAlTI2rbKV4gNFUOaotpLfdgA==", + "license": "MIT", + "dependencies": { + "@types/react-reconciler": "^0.28.9" + }, + "peerDependencies": { + "react": ">=17.0.1" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -1377,7 +1395,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1440,8 +1457,7 @@ "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "node_modules/debug": { "version": "4.4.3", @@ -1934,7 +1950,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -1954,7 +1969,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2129,7 +2143,6 @@ "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/package.json b/package.json index 36023af..704aca8 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.18", + "bippy": "^0.5.28", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", diff --git a/public/manifest.json b/public/manifest.json index 4fdaff2..47dd174 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -31,5 +31,11 @@ "16": "icons/icon-16.png", "48": "icons/icon-48.png", "128": "icons/icon-128.png" - } + }, + "web_accessible_resources": [ + { + "resources": ["react-extractor.js"], + "matches": [""] + } + ] } diff --git a/src/background/MessageRouter.ts b/src/background/MessageRouter.ts index d28bcc9..b5ff426 100644 --- a/src/background/MessageRouter.ts +++ b/src/background/MessageRouter.ts @@ -49,8 +49,26 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo) => { /** * Inject content script into a tab if not already injected. + * Also injects the React extractor script in the MAIN world for React source extraction. */ async function injectContentScript(tabId: number): Promise { + // Always try to inject the React extractor in MAIN world + // It doesn't persist across page navigations, so we need to inject it each time + // Multiple injections are safe (just adds redundant listeners) + try { + await chrome.scripting.executeScript({ + target: { tabId }, + files: ['react-extractor.js'], + world: 'MAIN', + }); + console.log('[MessageRouter] React extractor injected in MAIN world'); + } catch (e) { + // React extractor injection failure is non-fatal + // The extension will still work, just without React source info + console.warn('[MessageRouter] React extractor injection failed (non-fatal):', e); + } + + // Content script can be skipped if already injected if (injectedTabs.has(tabId)) { console.log('[MessageRouter] Content script already injected in tab:', tabId); return; @@ -58,6 +76,7 @@ async function injectContentScript(tabId: number): Promise { try { console.log('[MessageRouter] Injecting content script into tab:', tabId); + await chrome.scripting.executeScript({ target: { tabId }, files: ['content.js'], diff --git a/src/content/index.ts b/src/content/index.ts index 216ba24..569d933 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -8,11 +8,14 @@ */ import type { BackgroundToContentMessage } from '@/shared/messages'; -import type { CapturedCustomAttribute, CapturedElement, CustomAttribute } from '@/shared/types'; +import type { CapturedCustomAttribute, CapturedElement, CustomAttribute, ReactSourceInfo } from '@/shared/types'; import { normalizeAttributeName } from '@/shared/utils'; import { getBestSelector } from './SelectorGenerator'; import { DOM_CAPTURE_CONFIG } from '@/shared/constants'; +// Timeout for React source extraction (ms) +const REACT_SOURCE_TIMEOUT = 500; + // State let elementPickerActive = false; let highlightElement: HTMLDivElement | null = null; @@ -272,6 +275,55 @@ function createConfirmationHighlight(rect: DOMRect, index: number): void { }, 500); } +/** + * Request React source info from the main world script via postMessage. + * The main world script has access to React internals via window.__REACT_DEVTOOLS_GLOBAL_HOOK__. + */ +async function getReactSourceFromMainWorld(element: Element): Promise { + return new Promise((resolve) => { + const rect = element.getBoundingClientRect(); + const elementId = crypto.randomUUID(); + + console.log('[ClankerContext] Requesting React source for element:', element); + console.log('[ClankerContext] Element rect:', rect); + + const handler = (event: MessageEvent) => { + // Only accept messages from the same frame + if (event.source !== window) return; + + if ( + event.data?.type === 'CLANKER_REACT_SOURCE_RESULT' && + event.data.elementId === elementId + ) { + console.log('[ClankerContext] Received React source response:', event.data.reactSource); + window.removeEventListener('message', handler); + resolve(event.data.reactSource); + } + }; + + window.addEventListener('message', handler); + + // Send request to main world script + window.postMessage( + { + type: 'CLANKER_GET_REACT_SOURCE', + elementId, + x: rect.left + rect.width / 2, + y: rect.top + rect.height / 2, + }, + '*' + ); + console.log('[ClankerContext] Sent React source request with elementId:', elementId); + + // Timeout fallback - don't block if React extractor isn't available + setTimeout(() => { + console.log('[ClankerContext] React source request timed out'); + window.removeEventListener('message', handler); + resolve(null); + }, REACT_SOURCE_TIMEOUT); + }); +} + /** * Find a custom attribute value on or near an element. * Searches based on the configured direction: parent, descendant, or both. @@ -334,9 +386,9 @@ function findCustomAttribute( } /** - * Capture element data (HTML and selector). + * Capture element data (HTML, selector, custom attributes, and React source). */ -function captureElement(element: Element): CapturedElement { +async function captureElement(element: Element): Promise { let html = element.outerHTML; if (html.length > DOM_CAPTURE_CONFIG.MAX_OUTER_HTML_LENGTH) { html = html.substring(0, DOM_CAPTURE_CONFIG.MAX_OUTER_HTML_LENGTH) + ''; @@ -349,10 +401,14 @@ function captureElement(element: Element): CapturedElement { .map((config) => findCustomAttribute(element, config)) .filter((attr): attr is CapturedCustomAttribute => attr !== null); + // Try to get React source info from main world + const reactSource = await getReactSourceFromMainWorld(element); + return { html, selector, customAttributes: customAttributes.length > 0 ? customAttributes : undefined, + reactSource: reactSource ?? undefined, }; } @@ -611,8 +667,8 @@ async function handlePickerClick(event: MouseEvent): Promise { return; } - // Capture element data - const captured = captureElement(element); + // Capture element data (async to allow React source extraction) + const captured = await captureElement(element); // Add to selected elements selectedElements.push(captured); diff --git a/src/content/react-extractor.ts b/src/content/react-extractor.ts new file mode 100644 index 0000000..e008b08 --- /dev/null +++ b/src/content/react-extractor.ts @@ -0,0 +1,273 @@ +/** + * React Source Extractor - Main World Script + * + * This script runs in the MAIN world (page context) to access React internals + * via window.__REACT_DEVTOOLS_GLOBAL_HOOK__. It uses the bippy library to + * extract React component source information from DOM elements. + * + * Communication with the content script (isolated world) happens via postMessage. + */ + +import { + getFiberFromHostInstance, + getLatestFiber, + getDisplayName, + traverseFiber, + isCompositeFiber, + hasRDTHook, +} from 'bippy'; +import { getOwnerStack, getSource, isSourceFile, normalizeFileName } from 'bippy/source'; +import type { ReactSourceInfo } from '@/shared/types'; + +// Internal React component names to filter out +const INTERNAL_COMPONENTS = new Set([ + 'Suspense', + 'Fragment', + 'StrictMode', + 'Profiler', + 'Portal', + 'Provider', + 'Consumer', + 'Context', + 'ForwardRef', + 'Memo', + 'Lazy', +]); + +// Next.js internal components to filter out +const NEXTJS_INTERNALS = new Set([ + 'AppRouter', + 'InnerLayoutRouter', + 'OuterLayoutRouter', + 'RenderFromTemplateContext', + 'ScrollAndFocusHandler', + 'RedirectErrorBoundary', + 'NotFoundErrorBoundary', + 'DevRootNotFoundBoundary', + 'HotReload', + 'Router', + 'Head', + 'AppContainer', + 'Container', + 'ErrorBoundary', + 'ErrorBoundaryHandler', + 'LoadingBoundary', + 'TemplateContext', + 'ParallelRouteDefault', +]); + +/** + * Check if a component name is a user component (not internal/framework) + */ +function isUserComponent(name: string | null): boolean { + if (!name) return false; + + // Must be PascalCase (starts with uppercase) + if (!/^[A-Z]/.test(name)) return false; + + // Filter out React internals + if (INTERNAL_COMPONENTS.has(name)) return false; + + // Filter out Next.js internals + if (NEXTJS_INTERNALS.has(name)) return false; + + // Filter out anonymous components + if (name === 'Anonymous' || name === 'Unknown') return false; + + return true; +} + +/** + * Extract React source info from a DOM element using bippy + */ +async function extractReactSource(element: Element): Promise { + try { + // Check if React DevTools hook is available + if (!hasRDTHook()) { + console.log('[ClankerContext] React DevTools hook not available'); + return null; + } + + // Get the fiber from the DOM element + const fiber = getFiberFromHostInstance(element); + if (!fiber) { + console.log('[ClankerContext] No fiber found for element'); + return null; + } + + // Get the latest version of the fiber (handles double-buffering) + const latestFiber = getLatestFiber(fiber); + if (!latestFiber) { + return null; + } + + // Find the nearest composite fiber (actual component) going up the tree + let compositeFiber = latestFiber; + let found = false; + + traverseFiber( + latestFiber, + (f) => { + if (found) return true; // Stop traversal + if (isCompositeFiber(f)) { + const name = getDisplayName(f.type); + if (isUserComponent(name)) { + compositeFiber = f; + found = true; + return true; // Stop traversal + } + } + return false; + }, + true // ascending (go up the tree) + ); + + if (!found || !isCompositeFiber(compositeFiber)) { + console.log('[ClankerContext] No user component found in fiber tree'); + return null; + } + + // Get the component name + const componentName = getDisplayName(compositeFiber.type) || null; + + // Try to get source info + let filePath: string | null = null; + let lineNumber: number | null = null; + let columnNumber: number | null = null; + + try { + const source = await getSource(compositeFiber); + if (source) { + filePath = source.fileName ? normalizeFileName(source.fileName) : null; + lineNumber = source.lineNumber ?? null; + columnNumber = source.columnNumber ?? null; + } + } catch (e) { + console.log('[ClankerContext] Could not get source:', e); + } + + // Build component stack by traversing up the tree + const componentStack: string[] = []; + traverseFiber( + compositeFiber, + (f) => { + if (isCompositeFiber(f)) { + const name = getDisplayName(f.type); + if (isUserComponent(name) && name) { + componentStack.push(name); + } + } + // Limit stack depth + return componentStack.length >= 10; + }, + true // ascending + ); + + // If we couldn't get source from getSource, try getOwnerStack + if (!filePath && componentStack.length > 0) { + try { + const ownerStack = await getOwnerStack(compositeFiber); + if (ownerStack && ownerStack.length > 0) { + // Find the first frame that's a user source file + for (const frame of ownerStack) { + if (frame.fileName && isSourceFile(frame.fileName)) { + filePath = normalizeFileName(frame.fileName); + lineNumber = frame.lineNumber ?? null; + columnNumber = frame.columnNumber ?? null; + break; + } + } + } + } catch (e) { + console.log('[ClankerContext] Could not get owner stack:', e); + } + } + + return { + componentName, + filePath, + lineNumber, + columnNumber, + componentStack, + }; + } catch (e) { + console.error('[ClankerContext] Error extracting React source:', e); + return null; + } +} + +// Prevent duplicate initialization if script is injected multiple times +const INIT_FLAG = '__CLANKER_REACT_EXTRACTOR_INITIALIZED__'; + +/** + * Get the actual element under cursor, filtering out ClankerContext overlay elements. + * This mirrors the logic in the content script's getElementUnderCursor. + */ +function getElementUnderPoint(x: number, y: number): Element | null { + const elements = document.elementsFromPoint(x, y); + + for (const el of elements) { + // Skip ClankerContext picker elements by ID prefix + if (el.id?.startsWith('clankercontext-')) continue; + + // Skip by class prefix + if (Array.from(el.classList).some((c) => c.startsWith('clankercontext-'))) continue; + + // Skip body and html + if (el === document.body || el === document.documentElement) continue; + + return el; + } + + return null; +} + +if (!(window as any)[INIT_FLAG]) { + (window as any)[INIT_FLAG] = true; + + /** + * Handle messages from the content script requesting React source info + */ + window.addEventListener('message', async (event) => { + // Only accept messages from the same frame + if (event.source !== window) return; + + if (event.data?.type === 'CLANKER_GET_REACT_SOURCE') { + console.log('[ClankerContext] React extractor received request:', event.data); + const { elementId, x, y } = event.data; + + // Find the element at the specified coordinates, filtering out our overlay + const element = getElementUnderPoint(x, y); + console.log('[ClankerContext] Element at point:', element); + + let reactSource: ReactSourceInfo | null = null; + + if (element) { + reactSource = await extractReactSource(element); + console.log('[ClankerContext] Extracted React source:', reactSource); + } + + // Send the result back to the content script + window.postMessage( + { + type: 'CLANKER_REACT_SOURCE_RESULT', + elementId, + reactSource, + }, + '*' + ); + console.log('[ClankerContext] Sent response back'); + } + }); + + // Debug: Check if React DevTools hook exists + const hookExists = hasRDTHook(); + console.log('[ClankerContext] React extractor initialized'); + console.log('[ClankerContext] React DevTools hook exists:', hookExists); + if (hookExists) { + const hook = (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__; + console.log('[ClankerContext] Hook renderers:', hook?.renderers?.size); + } +} else { + console.log('[ClankerContext] React extractor already initialized, skipping'); +} diff --git a/src/exporter/MarkdownExporter.ts b/src/exporter/MarkdownExporter.ts index f4f4ac8..c08c758 100644 --- a/src/exporter/MarkdownExporter.ts +++ b/src/exporter/MarkdownExporter.ts @@ -72,6 +72,9 @@ class MarkdownExporter { const consoleErrorsMarkdown = this.buildConsoleErrorsMarkdown(consoleErrors); const networkErrorsTable = this.buildNetworkErrorsTable(networkErrors); + // React source info tokens + const reactSourceTokens = this.buildReactSourceTokens(issue.elements); + return { 'issue.id': issue.id, 'issue.name': issueName, @@ -98,6 +101,55 @@ class MarkdownExporter { network_errors_table: networkErrorsTable, errors_present: hasErrors, ...this.buildCustomAttributeTokens(issue.elements), + ...reactSourceTokens, + }; + } + + /** + * Build template context tokens for React source info. + * Uses the first element's React source if available. + */ + private buildReactSourceTokens(elements: CapturedElement[]): Record { + // Find the first element with React source info + const firstReactSource = elements.find((el) => el.reactSource)?.reactSource; + + if (!firstReactSource) { + return { + react_source_present: false, + 'react.component_name': '', + 'react.file_path': '', + 'react.line_number': '', + 'react.column_number': '', + 'react.component_stack': '', + 'react.file_location': '', + }; + } + + // Build file location string (e.g., "components/Button.tsx:42") + let fileLocation = ''; + if (firstReactSource.filePath) { + fileLocation = firstReactSource.filePath; + if (firstReactSource.lineNumber) { + fileLocation += `:${firstReactSource.lineNumber}`; + if (firstReactSource.columnNumber) { + fileLocation += `:${firstReactSource.columnNumber}`; + } + } + } + + // Build component stack string + const componentStackStr = firstReactSource.componentStack.length > 0 + ? firstReactSource.componentStack.map((name) => ` → ${name}`).join('\n') + : ''; + + return { + react_source_present: true, + 'react.component_name': firstReactSource.componentName || '', + 'react.file_path': firstReactSource.filePath || '', + 'react.line_number': firstReactSource.lineNumber?.toString() || '', + 'react.column_number': firstReactSource.columnNumber?.toString() || '', + 'react.component_stack': componentStackStr, + 'react.file_location': fileLocation, }; } diff --git a/src/prompts/templates.ts b/src/prompts/templates.ts index 27a165b..21e5dec 100644 --- a/src/prompts/templates.ts +++ b/src/prompts/templates.ts @@ -24,6 +24,18 @@ The user selected the following element(s) as the focus of their request: These element(s) may be the source of the bug, or closely related to it. Inspect their attributes, event handlers, and parent/child relationships. +{{#react_source_present}} +## React Component Location + +This element is rendered by a React component. Use this to quickly locate the source code: + +- **Component:** \`{{react.component_name}}\` +- **File:** \`{{react.file_location}}\` + +**Component Stack (nearest to root):** +{{react.component_stack}} +{{/react_source_present}} + {{#console_errors_present}} ## Console Errors @@ -72,6 +84,18 @@ The user selected the following element(s) as the focus of their request: Use these element(s) as reference for where to apply the enhancement. You may need to modify these elements, their parents, or add sibling elements. +{{#react_source_present}} +## React Component Location + +This element is rendered by a React component. Use this to quickly locate the source code: + +- **Component:** \`{{react.component_name}}\` +- **File:** \`{{react.file_location}}\` + +**Component Stack (nearest to root):** +{{react.component_stack}} +{{/react_source_present}} + {{#console_errors_present}} ## Console Errors diff --git a/src/shared/types.ts b/src/shared/types.ts index b66f299..3a3b0bb 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -24,6 +24,15 @@ export interface CapturedCustomAttribute { foundOn: 'selected' | 'parent' | 'descendant'; } +// React source location info (from bippy) +export interface ReactSourceInfo { + componentName: string | null; + filePath: string | null; + lineNumber: number | null; + columnNumber: number | null; + componentStack: string[]; // Full component ancestry +} + // Prompt template stored in IndexedDB export interface PromptTemplate { type: IssueType; @@ -36,6 +45,7 @@ export interface CapturedElement { html: string; // outerHTML of the element selector: string; // CSS selector for reference customAttributes?: CapturedCustomAttribute[]; // Captured custom attributes + reactSource?: ReactSourceInfo; // React component source info (if available) } // Issue - captured bug or enhancement request diff --git a/vite.config.ts b/vite.config.ts index 2a757de..19482ec 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -34,6 +34,35 @@ async function buildContentScript() { }); } +// React extractor runs in MAIN world - needs separate IIFE build +async function buildReactExtractor() { + await build({ + configFile: false, + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + build: { + outDir: 'dist', + emptyOutDir: false, // Don't clear - main build already ran + lib: { + entry: resolve(__dirname, 'src/content/react-extractor.ts'), + name: 'ClankerContextReactExtractor', + formats: ['iife'], + fileName: () => 'react-extractor.js', + }, + rollupOptions: { + output: { + extend: true, + }, + }, + minify: 'esbuild', + target: 'esnext', + }, + }); +} + export default defineConfig({ base: './', plugins: [ @@ -64,10 +93,12 @@ export default defineConfig({ }, }, { - name: 'build-content-script', + name: 'build-content-scripts', closeBundle: async () => { // Build content script as IIFE after main build await buildContentScript(); + // Build React extractor as IIFE (runs in MAIN world) + await buildReactExtractor(); }, }, ], From adc15f7c5cc877dcc91be6a284105a995a27a6d0 Mon Sep 17 00:00:00 2001 From: Devin Griffin Date: Sat, 31 Jan 2026 22:20:29 -0600 Subject: [PATCH 02/16] filter out minified react components as they would just be noise&wasted tokens --- package-lock.json | 6 ++++++ src/exporter/MarkdownExporter.ts | 22 ++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index be64b22..78dd3c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1276,6 +1277,7 @@ "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1395,6 +1397,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1950,6 +1953,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -1969,6 +1973,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2143,6 +2148,7 @@ "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/exporter/MarkdownExporter.ts b/src/exporter/MarkdownExporter.ts index c08c758..ce7d90c 100644 --- a/src/exporter/MarkdownExporter.ts +++ b/src/exporter/MarkdownExporter.ts @@ -1,4 +1,4 @@ -import type { CapturedElement, ConsoleError, Issue, NetworkError } from '@/shared/types'; +import type { CapturedElement, ConsoleError, Issue, NetworkError, ReactSourceInfo } from '@/shared/types'; import { storageManager } from '@/background/StorageManager'; import { DEFAULT_PROMPT_TEMPLATES } from '@/prompts/templates'; import { renderTemplate, type TemplateContext } from '@/exporter/PromptTemplateRenderer'; @@ -105,6 +105,23 @@ class MarkdownExporter { }; } + /** + * Detect if React source info appears to be from a minified build. + * If 3+ components in the stack have very short names (≤2 chars), + * it's likely minified and we should suppress the output. + */ + private isLikelyMinified(reactSource: ReactSourceInfo): boolean { + const shortNameCount = reactSource.componentStack.filter( + (name) => name.length <= 2 + ).length; + + // Also check the main component name + const mainNameShort = reactSource.componentName !== null && reactSource.componentName.length <= 2; + const totalShort = shortNameCount + (mainNameShort ? 1 : 0); + + return totalShort >= 3; + } + /** * Build template context tokens for React source info. * Uses the first element's React source if available. @@ -113,7 +130,8 @@ class MarkdownExporter { // Find the first element with React source info const firstReactSource = elements.find((el) => el.reactSource)?.reactSource; - if (!firstReactSource) { + // Return empty if no source OR if it looks minified + if (!firstReactSource || this.isLikelyMinified(firstReactSource)) { return { react_source_present: false, 'react.component_name': '', From 22d1671249af4623ea118cf1c32cff0037cb9435 Mon Sep 17 00:00:00 2001 From: Devin Griffin Date: Sat, 31 Jan 2026 22:28:12 -0600 Subject: [PATCH 03/16] tighten up settings padding --- src/popup/SettingsView.tsx | 39 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/popup/SettingsView.tsx b/src/popup/SettingsView.tsx index c3d84c0..8d04c3a 100644 --- a/src/popup/SettingsView.tsx +++ b/src/popup/SettingsView.tsx @@ -466,7 +466,7 @@ export function SettingsView({ onBack, onEditPrompt }: SettingsViewProps): React )} -
+
Connections @@ -536,15 +536,15 @@ export function SettingsView({ onBack, onEditPrompt }: SettingsViewProps): React
- Prompts + Prompt Templates @@ -556,26 +556,26 @@ export function SettingsView({ onBack, onEditPrompt }: SettingsViewProps): React
) : ( -
+
{promptTemplates.map((template) => { const lastUpdated = template.updatedAt - ? new Date(template.updatedAt).toLocaleString() + ? new Date(template.updatedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : null; - const status = template.isCustom - ? `Custom${lastUpdated ? ` - Updated ${lastUpdated}` : ''}` - : 'Default'; return (
-
- {template.label} prompt - {status} +
+ {template.label} + + {template.isCustom ? (lastUpdated ? `Custom - ${lastUpdated}` : 'Custom') : 'Default'} +
) : ( -
-
-
+
+
+
Auto Copy Context - - Automatically copy context to clipboard after logging an issue - + on log
', + selector: '#login-btn', + customAttributes: [ + { + name: 'data-testid', + tokenName: 'data_testid', + value: 'login-button', + foundOn: 'selected', + }, + ], + }, + ], + pageUrl: 'https://example.com/login', +}; + +/** + * Mock Issue for enhancement scenario. + */ +export const mockEnhancementIssue: Issue = { + id: 'issue-enh-456', + type: 'enhancement', + timestamp: 1706745600000, + name: 'Add Dark Mode Toggle', + userPrompt: 'Add a dark mode toggle button to the header', + elements: [ + { + html: '', + selector: 'header.site-header', + customAttributes: [ + { + name: 'data-component', + tokenName: 'data_component', + value: 'main-header', + foundOn: 'selected', + }, + ], + }, + ], + pageUrl: 'https://example.com/dashboard', +}; + +/** + * Mock Issue with multiple elements. + */ +export const mockMultiElementIssue: Issue = { + id: 'issue-multi-789', + type: 'fix', + timestamp: 1706745600000, + name: 'Form Validation Issue', + userPrompt: 'Form fields are not validating correctly', + elements: [ + { + html: '', + selector: '#email', + customAttributes: [ + { + name: 'data-testid', + tokenName: 'data_testid', + value: 'email-input', + foundOn: 'selected', + }, + ], + }, + { + html: '', + selector: '#password', + customAttributes: [], + }, + { + html: '', + selector: 'button.submit-btn', + customAttributes: [], + }, + ], + pageUrl: 'https://example.com/signup', +}; + +/** + * Mock Issue with no custom attributes. + */ +export const mockIssueNoCustomAttrs: Issue = { + id: 'issue-noattr-101', + type: 'fix', + timestamp: 1706745600000, + name: 'Button Style Issue', + userPrompt: 'Button has wrong background color', + elements: [ + { + html: '', + selector: 'button.btn', + }, + ], + pageUrl: 'https://example.com/page', +}; + +/** + * Mock Issue with empty user prompt. + */ +export const mockIssueEmptyPrompt: Issue = { + id: 'issue-empty-102', + type: 'fix', + timestamp: 1706745600000, + name: 'Unknown Issue', + userPrompt: '', + elements: [ + { + html: '
Content
', + selector: 'div.widget', + }, + ], + pageUrl: 'https://example.com/widget', +}; + +/** + * Mock ConsoleError array with various error types. + */ +export const mockConsoleErrors: ConsoleError[] = [ + { + timestamp: 1706745600000, + message: 'Uncaught TypeError: Cannot read property \'click\' of undefined', + stackTrace: 'at handleClick (app.js:42:15)\n at HTMLButtonElement. (app.js:50:10)', + url: 'https://example.com/app.js', + lineNumber: 42, + }, + { + timestamp: 1706745601000, + message: 'Error: Network request failed', + stackTrace: 'at fetchData (api.js:15:5)\n at async loadUser (user.js:8:3)', + url: 'https://example.com/api.js', + lineNumber: 15, + }, +]; + +/** + * Single console error for simpler tests. + */ +export const mockSingleConsoleError: ConsoleError[] = [ + { + timestamp: 1706745600000, + message: 'Uncaught TypeError: Cannot read property \'click\' of undefined', + stackTrace: 'at handleClick (app.js:42:15)\n at HTMLButtonElement. (app.js:50:10)', + url: 'https://example.com/app.js', + lineNumber: 42, + }, +]; + +/** + * Console error without stack trace. + */ +export const mockConsoleErrorNoStack: ConsoleError[] = [ + { + timestamp: 1706745600000, + message: 'Script error.', + url: undefined, + lineNumber: undefined, + }, +]; + +/** + * Empty console errors array. + */ +export const mockEmptyConsoleErrors: ConsoleError[] = []; + +/** + * Mock NetworkError array with various HTTP status codes. + */ +export const mockNetworkErrors: NetworkError[] = [ + { + timestamp: 1706745600000, + url: 'https://api.example.com/auth/login', + status: 500, + method: 'POST', + }, + { + timestamp: 1706745601000, + url: 'https://api.example.com/users/profile', + status: 404, + method: 'GET', + }, + { + timestamp: 1706745602000, + url: 'https://cdn.example.com/assets/missing.js', + status: 0, // CORS/Network error + method: 'GET', + }, +]; + +/** + * Single network error for simpler tests. + */ +export const mockSingleNetworkError: NetworkError[] = [ + { + timestamp: 1706745600000, + url: 'https://api.example.com/auth/login', + status: 500, + method: 'POST', + }, +]; + +/** + * Empty network errors array. + */ +export const mockEmptyNetworkErrors: NetworkError[] = []; + +/** + * Mock Issue with multiple custom attributes. + */ +export const mockIssueMultipleCustomAttrs: Issue = { + id: 'issue-multiattr-200', + type: 'fix', + timestamp: 1706745600000, + name: 'Complex Element Issue', + userPrompt: 'Element has interaction problems', + elements: [ + { + html: '
Content
', + selector: 'div.card', + customAttributes: [ + { + name: 'data-testid', + tokenName: 'data_testid', + value: 'main-card', + foundOn: 'selected', + }, + { + name: 'data-qa', + tokenName: 'data_qa', + value: 'card-123', + foundOn: 'selected', + }, + ], + }, + ], + pageUrl: 'https://example.com/cards', +}; diff --git a/src/__tests__/templates/v1.1.6/__snapshots__/customTemplates.test.ts.snap b/src/__tests__/templates/v1.1.6/__snapshots__/customTemplates.test.ts.snap new file mode 100644 index 0000000..d440b0f --- /dev/null +++ b/src/__tests__/templates/v1.1.6/__snapshots__/customTemplates.test.ts.snap @@ -0,0 +1,382 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Custom Templates v1.1.6 - Kitchen Sink Snapshot Tests should match snapshot for enhancement issue: kitchen-sink-enhancement 1`] = ` +"# Enhancement Report + +## Issue Metadata +- **ID:** issue-enh-456 +- **Name:** Add Dark Mode Toggle +- **Type:** enhancement +- **Label:** Modify +- **Title:** Enhancement +- **Page URL:** https://example.com/dashboard +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** Add a dark mode toggle button to the header + +**Blockquote format:** +> Add a dark mode toggle button to the header + +## Elements Overview +- **Count:** 1 element(s) +- **First element selector:** header.site-header + +### First Element (Code Block) +\`\`\`html + +\`\`\` +**CSS Selector:** \`header.site-header\` + +### First Element HTML (Plain) +\`\`\`html + +\`\`\` + +### All Elements (Full Markdown) +\`\`\`html + +\`\`\` + +**CSS Selector:** \`header.site-header\` + +### All Elements HTML Only +\`\`\`html + +\`\`\` + +### All Selectors Only +**CSS Selector:** \`header.site-header\` + + + + + + + + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; + +exports[`Custom Templates v1.1.6 - Kitchen Sink Snapshot Tests should match snapshot for kitchen sink with all data: kitchen-sink-all-data 1`] = ` +"# Bug Fix Report + +## Issue Metadata +- **ID:** issue-fix-123 +- **Name:** Login Button Bug +- **Type:** fix +- **Label:** Fix +- **Title:** Bug Fix +- **Page URL:** https://example.com/login +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** The login button does not respond when clicked + +**Blockquote format:** +> The login button does not respond when clicked + +## Elements Overview +- **Count:** 1 element(s) +- **First element selector:** #login-btn + +### First Element (Code Block) +\`\`\`html + +\`\`\` +**CSS Selector:** \`#login-btn\` + +### First Element HTML (Plain) +\`\`\`html + +\`\`\` + +### All Elements (Full Markdown) +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + +### All Elements HTML Only +\`\`\`html + +\`\`\` + +### All Selectors Only +**CSS Selector:** \`#login-btn\` + + +## Console Errors +**2 error(s) detected** + +### Error +\`\`\` +Uncaught TypeError: Cannot read property 'click' of undefined +\`\`\` + +**Stack trace:** +\`\`\` +at handleClick (app.js:42:15) + at HTMLButtonElement. (app.js:50:10) +\`\`\` +**Source:** \`https://example.com/app.js:42\` + +### Error +\`\`\` +Error: Network request failed +\`\`\` + +**Stack trace:** +\`\`\` +at fetchData (api.js:15:5) + at async loadUser (user.js:8:3) +\`\`\` +**Source:** \`https://example.com/api.js:15\` + + + +## Network Errors +**3 failed request(s)** + +| Status | Method | URL | +|--------|--------|-----| +| 500 | POST | \`https://api.example.com/auth/login\` | +| 404 | GET | \`https://api.example.com/users/profile\` | +| CORS/Network | GET | \`https://cdn.example.com/assets/missing.js\` | + + + +## Error Summary +This issue has associated errors that may help diagnose the problem. + + + +## Custom Attribute: data-testid +**Value:** login-button + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; + +exports[`Custom Templates v1.1.6 - Kitchen Sink Snapshot Tests should match snapshot for kitchen sink with multiple elements: kitchen-sink-multi-elements 1`] = ` +"# Bug Fix Report + +## Issue Metadata +- **ID:** issue-multi-789 +- **Name:** Form Validation Issue +- **Type:** fix +- **Label:** Fix +- **Title:** Bug Fix +- **Page URL:** https://example.com/signup +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** Form fields are not validating correctly + +**Blockquote format:** +> Form fields are not validating correctly + +## Elements Overview +- **Count:** 3 element(s) +- **First element selector:** #email + +### First Element (Code Block) +\`\`\`html + +\`\`\` +**CSS Selector:** \`#email\` + +### First Element HTML (Plain) +\`\`\`html + +\`\`\` + +### All Elements (Full Markdown) +### Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#email\` + +### Element 2 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#password\` + +### Element 3 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.submit-btn\` + +### All Elements HTML Only +### Element 1 + +\`\`\`html + +\`\`\` + +### Element 2 + +\`\`\`html + +\`\`\` + +### Element 3 + +\`\`\`html + +\`\`\` + +### All Selectors Only +- Element 1: \`#email\` +- Element 2: \`#password\` +- Element 3: \`button.submit-btn\` + + +## Console Errors +**2 error(s) detected** + +### Error +\`\`\` +Uncaught TypeError: Cannot read property 'click' of undefined +\`\`\` + +**Stack trace:** +\`\`\` +at handleClick (app.js:42:15) + at HTMLButtonElement. (app.js:50:10) +\`\`\` +**Source:** \`https://example.com/app.js:42\` + +### Error +\`\`\` +Error: Network request failed +\`\`\` + +**Stack trace:** +\`\`\` +at fetchData (api.js:15:5) + at async loadUser (user.js:8:3) +\`\`\` +**Source:** \`https://example.com/api.js:15\` + + + +## Network Errors +**3 failed request(s)** + +| Status | Method | URL | +|--------|--------|-----| +| 500 | POST | \`https://api.example.com/auth/login\` | +| 404 | GET | \`https://api.example.com/users/profile\` | +| CORS/Network | GET | \`https://cdn.example.com/assets/missing.js\` | + + + +## Error Summary +This issue has associated errors that may help diagnose the problem. + + + +## Custom Attribute: data-testid +**Value:** email-input + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; + +exports[`Custom Templates v1.1.6 - Kitchen Sink Snapshot Tests should match snapshot for kitchen sink with no errors: kitchen-sink-no-errors 1`] = ` +"# Bug Fix Report + +## Issue Metadata +- **ID:** issue-fix-123 +- **Name:** Login Button Bug +- **Type:** fix +- **Label:** Fix +- **Title:** Bug Fix +- **Page URL:** https://example.com/login +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** The login button does not respond when clicked + +**Blockquote format:** +> The login button does not respond when clicked + +## Elements Overview +- **Count:** 1 element(s) +- **First element selector:** #login-btn + +### First Element (Code Block) +\`\`\`html + +\`\`\` +**CSS Selector:** \`#login-btn\` + +### First Element HTML (Plain) +\`\`\`html + +\`\`\` + +### All Elements (Full Markdown) +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + +### All Elements HTML Only +\`\`\`html + +\`\`\` + +### All Selectors Only +**CSS Selector:** \`#login-btn\` + + + + + + + + +## Custom Attribute: data-testid +**Value:** login-button + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; diff --git a/src/__tests__/templates/v1.1.6/__snapshots__/defaultTemplates.test.ts.snap b/src/__tests__/templates/v1.1.6/__snapshots__/defaultTemplates.test.ts.snap new file mode 100644 index 0000000..e3a5485 --- /dev/null +++ b/src/__tests__/templates/v1.1.6/__snapshots__/defaultTemplates.test.ts.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Default Templates v1.1.6 Snapshot Tests should match snapshot for enhancement template: enhancement-template 1`] = ` +"# Enhancement + +## Your Task +The user wants to add or change functionality on their web page. Review the context below and implement the requested enhancement. + +## What the User Wants +> Add a dark mode toggle button to the header + +## Context +**Page URL:** \`https://example.com/dashboard\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request: + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`header.site-header\` + +Use these element(s) as reference for where to apply the enhancement. You may need to modify these elements, their parents, or add sibling elements. + + + + + +## Summary + +**Type:** Enhancement + +**Suggested approach:** +1. Locate the target element in the codebase using the CSS selector +2. Understand the current behavior and surrounding code +3. Implement the requested enhancement +4. Test that existing functionality is not broken +" +`; + +exports[`Default Templates v1.1.6 Snapshot Tests should match snapshot for fix template with errors: fix-template-with-errors 1`] = ` +"# Bug Fix + +## Your Task +The user has identified a bug that needs fixing. Review the context below, identify the root cause, and implement a fix. + +## What the User Wants +> The login button does not respond when clicked + +## Context +**Page URL:** \`https://example.com/login\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request: + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + +These element(s) may be the source of the bug, or closely related to it. Inspect their attributes, event handlers, and parent/child relationships. + + +## Console Errors + +**1 error(s) detected.** These may indicate the root cause of the issue: + +### Error +\`\`\` +Uncaught TypeError: Cannot read property 'click' of undefined +\`\`\` + +**Stack trace:** +\`\`\` +at handleClick (app.js:42:15) + at HTMLButtonElement. (app.js:50:10) +\`\`\` +**Source:** \`https://example.com/app.js:42\` + + + +## Failed Network Requests + +**1 failed request(s).** These may indicate API issues, missing resources, or server errors: + +| Status | Method | URL | +|--------|--------|-----| +| 500 | POST | \`https://api.example.com/auth/login\` | + + +## Summary + +**Type:** Bug Fix + +**Suggested approach:** +1. Review the error messages and stack traces for clues +2. Locate the target element and related code +3. Identify the root cause of the issue +4. Implement and test the fix +" +`; + +exports[`Default Templates v1.1.6 Snapshot Tests should match snapshot for fix template without errors: fix-template-without-errors 1`] = ` +"# Bug Fix + +## Your Task +The user has identified a bug that needs fixing. Review the context below, identify the root cause, and implement a fix. + +## What the User Wants +> The login button does not respond when clicked + +## Context +**Page URL:** \`https://example.com/login\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request: + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + +These element(s) may be the source of the bug, or closely related to it. Inspect their attributes, event handlers, and parent/child relationships. + + + + + +## Summary + +**Type:** Bug Fix + +**Suggested approach:** +1. Review the error messages and stack traces for clues +2. Locate the target element and related code +3. Identify the root cause of the issue +4. Implement and test the fix +" +`; diff --git a/src/__tests__/templates/v1.1.6/customTemplates.test.ts b/src/__tests__/templates/v1.1.6/customTemplates.test.ts new file mode 100644 index 0000000..d462228 --- /dev/null +++ b/src/__tests__/templates/v1.1.6/customTemplates.test.ts @@ -0,0 +1,544 @@ +/** + * v1.1.6 - Custom Template Tests (Kitchen Sink) + * + * Tests a custom template using ALL 25 available tags at v1.1.6. + * This ensures backward compatibility as the template system evolves. + * + * When new versions are created, this file should NOT be modified. + * Instead, create a new version folder with updated tests. + * + * Breaking Change Detection: + * - All version tests run against the CURRENT renderTemplate() code + * - If a tag is renamed/removed, these tests will FAIL + * - The test "all tokens render correctly" catches leftover {{...}} tokens + */ + +import { renderTemplate } from '@/exporter/PromptTemplateRenderer'; +import { + mockFixIssue, + mockEnhancementIssue, + mockMultiElementIssue, + mockIssueNoCustomAttrs, + mockIssueEmptyPrompt, + mockIssueMultipleCustomAttrs, + mockConsoleErrors, + mockEmptyConsoleErrors, + mockNetworkErrors, + mockEmptyNetworkErrors, +} from './__mocks__/testData'; +import type { Issue, ConsoleError, NetworkError, CapturedElement } from '@/shared/types'; + +/** + * Kitchen sink template using ALL 25 tags available at v1.1.6. + * + * Issue Metadata (9 tags): + * - {{issue.id}}, {{issue.name}}, {{issue.type}} + * - {{issue.type_label}}, {{issue.type_title}} + * - {{issue.page_url}}, {{issue.user_prompt}} + * - {{issue.user_prompt_blockquote}}, {{issue.timestamp_iso}} + * + * Elements (8 tags): + * - {{elements_count}}, {{elements_markdown}} + * - {{elements_html_markdown}}, {{elements_selectors_markdown}} + * - {{elements_html_first}}, {{elements_selector_first}} + * - {{element.html}}, {{element.css_selector}} + * + * Console Errors (3 tags): + * - {{console_errors_count}}, {{#console_errors_present}}, {{console_errors_markdown}} + * + * Network Errors (3 tags): + * - {{network_errors_count}}, {{#network_errors_present}}, {{network_errors_table}} + * + * Combined (1 tag): + * - {{#errors_present}} + * + * Custom Attributes (dynamic): + * - {{data_testid}}, {{#data_testid_present}} + */ +const KITCHEN_SINK_TEMPLATE = `# {{issue.type_title}} Report + +## Issue Metadata +- **ID:** {{issue.id}} +- **Name:** {{issue.name}} +- **Type:** {{issue.type}} +- **Label:** {{issue.type_label}} +- **Title:** {{issue.type_title}} +- **Page URL:** {{issue.page_url}} +- **Timestamp:** {{issue.timestamp_iso}} + +## User Request +**Raw prompt:** {{issue.user_prompt}} + +**Blockquote format:** +{{issue.user_prompt_blockquote}} + +## Elements Overview +- **Count:** {{elements_count}} element(s) +- **First element selector:** {{elements_selector_first}} + +### First Element (Code Block) +{{element.html}} +{{element.css_selector}} + +### First Element HTML (Plain) +\`\`\`html +{{elements_html_first}} +\`\`\` + +### All Elements (Full Markdown) +{{elements_markdown}} + +### All Elements HTML Only +{{elements_html_markdown}} + +### All Selectors Only +{{elements_selectors_markdown}} + +{{#console_errors_present}} +## Console Errors +**{{console_errors_count}} error(s) detected** + +{{console_errors_markdown}} +{{/console_errors_present}} + +{{#network_errors_present}} +## Network Errors +**{{network_errors_count}} failed request(s)** + +| Status | Method | URL | +|--------|--------|-----| +{{network_errors_table}} +{{/network_errors_present}} + +{{#errors_present}} +## Error Summary +This issue has associated errors that may help diagnose the problem. +{{/errors_present}} + +{{#data_testid_present}} +## Custom Attribute: data-testid +**Value:** {{data_testid}} +{{/data_testid_present}} + +{{#data_qa_present}} +## Custom Attribute: data-qa +**Value:** {{data_qa}} +{{/data_qa_present}} + +--- +*Generated at {{issue.timestamp_iso}}* +`; + +/** + * Build template context (same logic as MarkdownExporter) + */ +function buildTestContext( + issue: Issue, + consoleErrors: ConsoleError[], + networkErrors: NetworkError[] +): Record { + const isEnhancement = issue.type === 'enhancement'; + const issueName = issue.name || 'Untitled'; + const hasErrors = consoleErrors.length > 0 || networkErrors.length > 0; + + const userPromptBlockquote = issue.userPrompt + ? `> ${issue.userPrompt.split('\n').join('\n> ')}` + : '_No description provided. Examine the selected element and errors for context._'; + + return { + 'issue.id': issue.id, + 'issue.name': issueName, + 'issue.type': issue.type, + 'issue.type_label': isEnhancement ? 'Modify' : 'Fix', + 'issue.type_title': isEnhancement ? 'Enhancement' : 'Bug Fix', + 'issue.page_url': issue.pageUrl, + 'issue.user_prompt': issue.userPrompt || '', + 'issue.user_prompt_blockquote': userPromptBlockquote, + 'issue.timestamp_iso': new Date(issue.timestamp).toISOString(), + elements_count: issue.elements.length, + elements_markdown: buildElementsMarkdown(issue.elements), + elements_html_markdown: buildElementsHtmlMarkdown(issue.elements), + elements_selectors_markdown: buildElementsSelectorsMarkdown(issue.elements), + elements_html_first: issue.elements[0] ? formatHTML(issue.elements[0].html) : '', + elements_selector_first: issue.elements[0]?.selector || '', + 'element.html': issue.elements[0] ? `\`\`\`html\n${formatHTML(issue.elements[0].html)}\n\`\`\`` : '', + 'element.css_selector': issue.elements[0] ? `**CSS Selector:** \`${issue.elements[0].selector}\`` : '', + console_errors_count: consoleErrors.length, + console_errors_present: consoleErrors.length > 0, + console_errors_markdown: buildConsoleErrorsMarkdown(consoleErrors), + network_errors_count: networkErrors.length, + network_errors_present: networkErrors.length > 0, + network_errors_table: buildNetworkErrorsTable(networkErrors), + errors_present: hasErrors, + ...buildCustomAttributeTokens(issue.elements), + }; +} + +function formatHTML(html: string): string { + let formatted = html.replace(/>\n<').replace(/>\s+\n<'); + if (formatted.length > 5000) { + formatted = formatted.substring(0, 5000) + '\n'; + } + return formatted; +} + +function buildElementsMarkdown(elements: CapturedElement[]): string { + if (elements.length === 0) return ''; + const lines: string[] = []; + if (elements.length === 1) { + lines.push('```html'); + lines.push(formatHTML(elements[0].html)); + lines.push('```'); + lines.push(''); + lines.push(`**CSS Selector:** \`${elements[0].selector}\``); + } else { + elements.forEach((element, index) => { + lines.push(`### Element ${index + 1}`); + lines.push(''); + lines.push('```html'); + lines.push(formatHTML(element.html)); + lines.push('```'); + lines.push(''); + lines.push(`**CSS Selector:** \`${element.selector}\``); + if (index < elements.length - 1) { + lines.push(''); + } + }); + } + return lines.join('\n').trimEnd(); +} + +function buildElementsHtmlMarkdown(elements: CapturedElement[]): string { + if (elements.length === 0) return ''; + const lines: string[] = []; + if (elements.length === 1) { + lines.push('```html'); + lines.push(formatHTML(elements[0].html)); + lines.push('```'); + } else { + elements.forEach((element, index) => { + lines.push(`### Element ${index + 1}`); + lines.push(''); + lines.push('```html'); + lines.push(formatHTML(element.html)); + lines.push('```'); + if (index < elements.length - 1) { + lines.push(''); + } + }); + } + return lines.join('\n').trimEnd(); +} + +function buildElementsSelectorsMarkdown(elements: CapturedElement[]): string { + if (elements.length === 0) return ''; + if (elements.length === 1) { + return `**CSS Selector:** \`${elements[0].selector}\``; + } + return elements.map((element, index) => `- Element ${index + 1}: \`${element.selector}\``).join('\n'); +} + +function truncate(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return str.substring(0, maxLen - 3) + '...'; +} + +function buildConsoleErrorsMarkdown(consoleErrors: ConsoleError[]): string { + if (consoleErrors.length === 0) return ''; + const lines: string[] = []; + const errorsToShow = consoleErrors.slice(0, 15); + errorsToShow.forEach((error, index) => { + lines.push('### Error'); + lines.push('```'); + lines.push(truncate(error.message, 500)); + lines.push('```'); + if (error.stackTrace) { + lines.push(''); + lines.push('**Stack trace:**'); + lines.push('```'); + const stackLines = error.stackTrace.split('\n').slice(0, 5); + lines.push(stackLines.join('\n')); + lines.push('```'); + } + if (error.url) { + lines.push(`**Source:** \`${error.url}${error.lineNumber ? `:${error.lineNumber}` : ''}\``); + } + if (index < errorsToShow.length - 1) { + lines.push(''); + } + }); + return lines.join('\n').trimEnd(); +} + +function buildNetworkErrorsTable(networkErrors: NetworkError[]): string { + if (networkErrors.length === 0) return ''; + const errorsToShow = networkErrors.slice(0, 15); + const lines = errorsToShow.map((error) => { + const shortUrl = truncate(error.url, 80); + const status = error.status === 0 ? 'CORS/Network' : error.status.toString(); + return `| ${status} | ${error.method} | \`${shortUrl}\` |`; + }); + return lines.join('\n'); +} + +function buildCustomAttributeTokens(elements: CapturedElement[]): Record { + const customAttrMap = new Map(); + for (const element of elements) { + if (element.customAttributes) { + for (const attr of element.customAttributes) { + if (!customAttrMap.has(attr.tokenName)) { + customAttrMap.set(attr.tokenName, attr.value); + } + } + } + } + const tokens: Record = {}; + for (const [tokenName, value] of customAttrMap) { + tokens[tokenName] = value; + tokens[`${tokenName}_present`] = true; + } + return tokens; +} + +describe('Custom Templates v1.1.6 - Kitchen Sink', () => { + describe('All Tags Render Correctly', () => { + it('should render all tags with fix issue and all errors', () => { + const context = buildTestContext(mockFixIssue, mockConsoleErrors, mockNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + // Critical assertion: no unrendered tokens should remain + expect(result).toHaveNoUnrenderedTokens(); + + // Verify issue metadata tags + expect(result).toContain('issue-fix-123'); + expect(result).toContain('Login Button Bug'); + expect(result).toContain('fix'); + expect(result).toContain('Fix'); + expect(result).toContain('Bug Fix'); + expect(result).toContain('https://example.com/login'); + expect(result).toContain('2024-02-01'); + + // Verify user prompt + expect(result).toContain('The login button does not respond when clicked'); + expect(result).toContain('> The login button does not respond when clicked'); + + // Verify elements + expect(result).toContain('1 element(s)'); + expect(result).toContain('#login-btn'); + expect(result).toContain(''); + expect(result).toContain('```'); + }); + + it('should include CSS selector', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + + expect(result).toContain('**CSS Selector:** `#login-btn`'); + }); + }); + + describe('Enhancement Template', () => { + it('should render enhancement template with all data present', () => { + const context = buildTestContext(mockEnhancementIssue, mockConsoleErrors, mockNetworkErrors); + const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.enhancement, context); + + // Check main sections + expect(result).toContain('# Enhancement'); + expect(result).toContain('## Your Task'); + expect(result).toContain('## What the User Wants'); + expect(result).toContain('## Context'); + expect(result).toContain('## Target Element(s)'); + expect(result).toContain('## Summary'); + + // Check content + expect(result).toContain('> Add a dark mode toggle button to the header'); + expect(result).toContain('https://example.com/dashboard'); + + // Verify no unrendered tokens remain + expect(result).toHaveNoUnrenderedTokens(); + }); + + it('should render enhancement template without errors', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.enhancement, context); + + expect(result).toContain('# Enhancement'); + expect(result).not.toContain('## Console Errors'); + expect(result).not.toContain('## Failed Network Requests'); + expect(result).toHaveNoUnrenderedTokens(); + }); + + it('should include suggested approach for enhancement', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.enhancement, context); + + expect(result).toContain('**Suggested approach:**'); + expect(result).toContain('Locate the target element in the codebase'); + expect(result).toContain('Implement the requested enhancement'); + }); + }); + + describe('Template Structure Validation', () => { + it('fix template should have expected structure', () => { + expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('# Bug Fix'); + expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{issue.user_prompt}}'); + expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{issue.page_url}}'); + expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{elements_markdown}}'); + expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{#console_errors_present}}'); + expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{/console_errors_present}}'); + expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{#network_errors_present}}'); + expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{/network_errors_present}}'); + }); + + it('enhancement template should have expected structure', () => { + expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('# Enhancement'); + expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{issue.user_prompt}}'); + expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{issue.page_url}}'); + expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{elements_markdown}}'); + expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{#console_errors_present}}'); + expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{/console_errors_present}}'); + }); + }); + + describe('Snapshot Tests', () => { + it('should match snapshot for fix template with errors', () => { + const context = buildTestContext(mockFixIssue, mockSingleConsoleError, mockSingleNetworkError); + const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + + expect(result).toMatchSnapshot('fix-template-with-errors'); + }); + + it('should match snapshot for fix template without errors', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + + expect(result).toMatchSnapshot('fix-template-without-errors'); + }); + + it('should match snapshot for enhancement template', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.enhancement, context); + + expect(result).toMatchSnapshot('enhancement-template'); + }); + }); +}); From 5edeaf0e11ca5c0aa12867a002218ff3626b9390 Mon Sep 17 00:00:00 2001 From: DNGriffin Date: Sun, 1 Feb 2026 16:01:07 -0600 Subject: [PATCH 08/16] merge unit-tests --- package-lock.json | 946 +--------------------------------------------- 1 file changed, 10 insertions(+), 936 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb66860..86643cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -543,246 +543,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", @@ -798,96 +558,6 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1253,268 +923,28 @@ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { + "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", "cpu": [ "x64" ], "optional": true, "os": [ - "win32" + "linux" ] }, - "node_modules/@rollup/rollup-win32-x64-msvc": { + "node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", "cpu": [ "x64" ], "optional": true, "os": [ - "win32" + "linux" ] }, "node_modules/@sinclair/typebox": { @@ -1580,111 +1010,6 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", @@ -1715,64 +1040,6 @@ "node": ">= 10" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@tailwindcss/vite": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", @@ -1930,11 +1197,6 @@ "version": "18.3.27", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", -<<<<<<< HEAD - "peer": true, -======= - "dev": true, ->>>>>>> dg/unit-tests "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1949,7 +1211,6 @@ "@types/react": "^18.0.0" } }, -<<<<<<< HEAD "node_modules/@types/react-reconciler": { "version": "0.28.9", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", @@ -1959,7 +1220,6 @@ "@types/react": "*" } }, -======= "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1984,7 +1244,6 @@ "dev": true, "license": "MIT" }, ->>>>>>> dg/unit-tests "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -2239,7 +1498,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, -<<<<<<< HEAD "node_modules/bippy": { "version": "0.5.28", "resolved": "https://registry.npmjs.org/bippy/-/bippy-0.5.28.tgz", @@ -2250,7 +1508,8 @@ }, "peerDependencies": { "react": ">=17.0.1" -======= + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2273,7 +1532,6 @@ }, "engines": { "node": ">=8" ->>>>>>> dg/unit-tests } }, "node_modules/browserslist": { @@ -2858,19 +2116,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3920,139 +3165,6 @@ "lightningcss-win32-x64-msvc": "1.30.2" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.2", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", @@ -4091,44 +3203,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", From c152b8ec1af938034f8e5413881a281001fd517c Mon Sep 17 00:00:00 2001 From: DNGriffin Date: Sun, 1 Feb 2026 16:16:06 -0600 Subject: [PATCH 09/16] update tests to use frozen versioned templates --- .../templates/v1.1.6/__mocks__/testData.ts | 101 ++++++++++++++++++ .../templates/v1.1.6/defaultTemplates.test.ts | 62 ++++++----- 2 files changed, 134 insertions(+), 29 deletions(-) diff --git a/src/__tests__/templates/v1.1.6/__mocks__/testData.ts b/src/__tests__/templates/v1.1.6/__mocks__/testData.ts index 9ef63c9..99e1907 100644 --- a/src/__tests__/templates/v1.1.6/__mocks__/testData.ts +++ b/src/__tests__/templates/v1.1.6/__mocks__/testData.ts @@ -253,3 +253,104 @@ export const mockIssueMultipleCustomAttrs: Issue = { ], pageUrl: 'https://example.com/cards', }; + +/** + * v1.1.6 frozen templates - DO NOT MODIFY + * These represent the template format at v1.1.6 for backward compatibility testing. + * They use the {{elements_markdown}} token which should continue to work. + */ +export const V1_1_6_FIX_TEMPLATE = `# Bug Fix + +## Your Task +The user has identified a bug that needs fixing. Review the context below, identify the root cause, and implement a fix. + +## What the User Wants +> {{issue.user_prompt}} + +## Context +**Page URL:** \`{{issue.page_url}}\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request: + +{{elements_markdown}} + +These element(s) may be the source of the bug, or closely related to it. Inspect their attributes, event handlers, and parent/child relationships. + +{{#console_errors_present}} +## Console Errors + +**{{console_errors_count}} error(s) detected.** These may indicate the root cause of the issue: + +{{console_errors_markdown}} +{{/console_errors_present}} + +{{#network_errors_present}} +## Failed Network Requests + +**{{network_errors_count}} failed request(s).** These may indicate API issues, missing resources, or server errors: + +| Status | Method | URL | +|--------|--------|-----| +{{network_errors_table}} +{{/network_errors_present}} + +## Summary + +**Type:** Bug Fix + +**Suggested approach:** +1. Review the error messages and stack traces for clues +2. Locate the target element and related code +3. Identify the root cause of the issue +4. Implement and test the fix +`; + +export const V1_1_6_ENHANCEMENT_TEMPLATE = `# Enhancement + +## Your Task +The user wants to add or change functionality on their web page. Review the context below and implement the requested enhancement. + +## What the User Wants +> {{issue.user_prompt}} + +## Context +**Page URL:** \`{{issue.page_url}}\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request: + +{{elements_markdown}} + +Use these element(s) as reference for where to apply the enhancement. You may need to modify these elements, their parents, or add sibling elements. + +{{#console_errors_present}} +## Console Errors + +**{{console_errors_count}} error(s) detected.** These may indicate the root cause of the issue: + +{{console_errors_markdown}} +{{/console_errors_present}} + +{{#network_errors_present}} +## Failed Network Requests + +**{{network_errors_count}} failed request(s).** These may indicate API issues, missing resources, or server errors: + +| Status | Method | URL | +|--------|--------|-----| +{{network_errors_table}} +{{/network_errors_present}} + +## Summary + +**Type:** Enhancement + +**Suggested approach:** +1. Locate the target element in the codebase using the CSS selector +2. Understand the current behavior and surrounding code +3. Implement the requested enhancement +4. Test that existing functionality is not broken +`; diff --git a/src/__tests__/templates/v1.1.6/defaultTemplates.test.ts b/src/__tests__/templates/v1.1.6/defaultTemplates.test.ts index 947be46..79d68f1 100644 --- a/src/__tests__/templates/v1.1.6/defaultTemplates.test.ts +++ b/src/__tests__/templates/v1.1.6/defaultTemplates.test.ts @@ -1,13 +1,15 @@ /** * v1.1.6 - Default Template Tests * - * Tests DEFAULT_PROMPT_TEMPLATES.fix and DEFAULT_PROMPT_TEMPLATES.enhancement - * to ensure they render correctly with various data scenarios. + * Tests backward compatibility: verifies that the v1.1.6 template format + * (using {{elements_markdown}} token) continues to render correctly. + * + * These tests use FROZEN template strings to ensure backward compatibility + * is maintained even when DEFAULT_PROMPT_TEMPLATES evolves. * * DO NOT MODIFY once a new version folder is created. */ -import { DEFAULT_PROMPT_TEMPLATES } from '@/prompts/templates'; import { renderTemplate } from '@/exporter/PromptTemplateRenderer'; import { mockFixIssue, @@ -18,6 +20,8 @@ import { mockSingleNetworkError, mockNetworkErrors, mockEmptyNetworkErrors, + V1_1_6_FIX_TEMPLATE, + V1_1_6_ENHANCEMENT_TEMPLATE, } from './__mocks__/testData'; import type { Issue, ConsoleError, NetworkError, CapturedElement } from '@/shared/types'; @@ -198,7 +202,7 @@ describe('Default Templates v1.1.6', () => { describe('Fix Template', () => { it('should render fix template with all data present', () => { const context = buildTestContext(mockFixIssue, mockConsoleErrors, mockNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + const result = renderTemplate(V1_1_6_FIX_TEMPLATE, context); // Check main sections are present expect(result).toContain('# Bug Fix'); @@ -225,7 +229,7 @@ describe('Default Templates v1.1.6', () => { it('should render fix template without console errors', () => { const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + const result = renderTemplate(V1_1_6_FIX_TEMPLATE, context); expect(result).toContain('# Bug Fix'); expect(result).not.toContain('## Console Errors'); @@ -235,7 +239,7 @@ describe('Default Templates v1.1.6', () => { it('should render fix template without network errors', () => { const context = buildTestContext(mockFixIssue, mockConsoleErrors, mockEmptyNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + const result = renderTemplate(V1_1_6_FIX_TEMPLATE, context); expect(result).toContain('# Bug Fix'); expect(result).toContain('## Console Errors'); @@ -245,7 +249,7 @@ describe('Default Templates v1.1.6', () => { it('should render fix template without any errors', () => { const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + const result = renderTemplate(V1_1_6_FIX_TEMPLATE, context); expect(result).toContain('# Bug Fix'); expect(result).not.toContain('## Console Errors'); @@ -255,7 +259,7 @@ describe('Default Templates v1.1.6', () => { it('should include element HTML in code block', () => { const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + const result = renderTemplate(V1_1_6_FIX_TEMPLATE, context); expect(result).toContain('```html'); expect(result).toContain(''); @@ -264,7 +268,7 @@ describe('Default Templates v1.1.6', () => { it('should include CSS selector', () => { const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + const result = renderTemplate(V1_1_6_FIX_TEMPLATE, context); expect(result).toContain('**CSS Selector:** `#login-btn`'); }); @@ -273,7 +277,7 @@ describe('Default Templates v1.1.6', () => { describe('Enhancement Template', () => { it('should render enhancement template with all data present', () => { const context = buildTestContext(mockEnhancementIssue, mockConsoleErrors, mockNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.enhancement, context); + const result = renderTemplate(V1_1_6_ENHANCEMENT_TEMPLATE, context); // Check main sections expect(result).toContain('# Enhancement'); @@ -293,7 +297,7 @@ describe('Default Templates v1.1.6', () => { it('should render enhancement template without errors', () => { const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.enhancement, context); + const result = renderTemplate(V1_1_6_ENHANCEMENT_TEMPLATE, context); expect(result).toContain('# Enhancement'); expect(result).not.toContain('## Console Errors'); @@ -303,7 +307,7 @@ describe('Default Templates v1.1.6', () => { it('should include suggested approach for enhancement', () => { const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.enhancement, context); + const result = renderTemplate(V1_1_6_ENHANCEMENT_TEMPLATE, context); expect(result).toContain('**Suggested approach:**'); expect(result).toContain('Locate the target element in the codebase'); @@ -313,44 +317,44 @@ describe('Default Templates v1.1.6', () => { describe('Template Structure Validation', () => { it('fix template should have expected structure', () => { - expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('# Bug Fix'); - expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{issue.user_prompt}}'); - expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{issue.page_url}}'); - expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{elements_markdown}}'); - expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{#console_errors_present}}'); - expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{/console_errors_present}}'); - expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{#network_errors_present}}'); - expect(DEFAULT_PROMPT_TEMPLATES.fix).toContain('{{/network_errors_present}}'); + expect(V1_1_6_FIX_TEMPLATE).toContain('# Bug Fix'); + expect(V1_1_6_FIX_TEMPLATE).toContain('{{issue.user_prompt}}'); + expect(V1_1_6_FIX_TEMPLATE).toContain('{{issue.page_url}}'); + expect(V1_1_6_FIX_TEMPLATE).toContain('{{elements_markdown}}'); + expect(V1_1_6_FIX_TEMPLATE).toContain('{{#console_errors_present}}'); + expect(V1_1_6_FIX_TEMPLATE).toContain('{{/console_errors_present}}'); + expect(V1_1_6_FIX_TEMPLATE).toContain('{{#network_errors_present}}'); + expect(V1_1_6_FIX_TEMPLATE).toContain('{{/network_errors_present}}'); }); it('enhancement template should have expected structure', () => { - expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('# Enhancement'); - expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{issue.user_prompt}}'); - expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{issue.page_url}}'); - expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{elements_markdown}}'); - expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{#console_errors_present}}'); - expect(DEFAULT_PROMPT_TEMPLATES.enhancement).toContain('{{/console_errors_present}}'); + expect(V1_1_6_ENHANCEMENT_TEMPLATE).toContain('# Enhancement'); + expect(V1_1_6_ENHANCEMENT_TEMPLATE).toContain('{{issue.user_prompt}}'); + expect(V1_1_6_ENHANCEMENT_TEMPLATE).toContain('{{issue.page_url}}'); + expect(V1_1_6_ENHANCEMENT_TEMPLATE).toContain('{{elements_markdown}}'); + expect(V1_1_6_ENHANCEMENT_TEMPLATE).toContain('{{#console_errors_present}}'); + expect(V1_1_6_ENHANCEMENT_TEMPLATE).toContain('{{/console_errors_present}}'); }); }); describe('Snapshot Tests', () => { it('should match snapshot for fix template with errors', () => { const context = buildTestContext(mockFixIssue, mockSingleConsoleError, mockSingleNetworkError); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + const result = renderTemplate(V1_1_6_FIX_TEMPLATE, context); expect(result).toMatchSnapshot('fix-template-with-errors'); }); it('should match snapshot for fix template without errors', () => { const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.fix, context); + const result = renderTemplate(V1_1_6_FIX_TEMPLATE, context); expect(result).toMatchSnapshot('fix-template-without-errors'); }); it('should match snapshot for enhancement template', () => { const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); - const result = renderTemplate(DEFAULT_PROMPT_TEMPLATES.enhancement, context); + const result = renderTemplate(V1_1_6_ENHANCEMENT_TEMPLATE, context); expect(result).toMatchSnapshot('enhancement-template'); }); From a0b949458131d84fbfa56a5466a1b977dcf5f5dc Mon Sep 17 00:00:00 2001 From: DNGriffin Date: Sun, 1 Feb 2026 16:36:25 -0600 Subject: [PATCH 10/16] add react template tests --- .../v1.1.8/PromptTemplateRenderer.test.ts | 689 +++++++++++++++ .../templates/v1.1.8/__mocks__/testData.ts | 664 +++++++++++++++ .../customTemplates.test.ts.snap | 668 +++++++++++++++ .../defaultTemplates.test.ts.snap | 320 +++++++ .../__snapshots__/quickSelect.test.ts.snap | 104 +++ .../templates/v1.1.8/customTemplates.test.ts | 793 ++++++++++++++++++ .../templates/v1.1.8/defaultTemplates.test.ts | 595 +++++++++++++ .../templates/v1.1.8/quickSelect.test.ts | 403 +++++++++ 8 files changed, 4236 insertions(+) create mode 100644 src/__tests__/templates/v1.1.8/PromptTemplateRenderer.test.ts create mode 100644 src/__tests__/templates/v1.1.8/__mocks__/testData.ts create mode 100644 src/__tests__/templates/v1.1.8/__snapshots__/customTemplates.test.ts.snap create mode 100644 src/__tests__/templates/v1.1.8/__snapshots__/defaultTemplates.test.ts.snap create mode 100644 src/__tests__/templates/v1.1.8/__snapshots__/quickSelect.test.ts.snap create mode 100644 src/__tests__/templates/v1.1.8/customTemplates.test.ts create mode 100644 src/__tests__/templates/v1.1.8/defaultTemplates.test.ts create mode 100644 src/__tests__/templates/v1.1.8/quickSelect.test.ts diff --git a/src/__tests__/templates/v1.1.8/PromptTemplateRenderer.test.ts b/src/__tests__/templates/v1.1.8/PromptTemplateRenderer.test.ts new file mode 100644 index 0000000..a42e4dc --- /dev/null +++ b/src/__tests__/templates/v1.1.8/PromptTemplateRenderer.test.ts @@ -0,0 +1,689 @@ +/** + * v1.1.8 - Core PromptTemplateRenderer tests + * + * Extends v1.1.6 tests to include: + * - {{#each arrayName}}...{{/each}} iteration blocks + * - Special variables: @index, @number, @first, @last, @count + * - Nested sections inside each blocks + * - element. prefix requirement for item tokens + * + * DO NOT MODIFY once a new version folder is created. + */ + +import { renderTemplate, type TemplateContext, type TemplateContextWithArrays, type TemplateArrayItem } from '@/exporter/PromptTemplateRenderer'; + +describe('PromptTemplateRenderer v1.1.8', () => { + // ============================================================================ + // Token Replacement (unchanged from v1.1.6) + // ============================================================================ + + describe('Token Replacement', () => { + it('should replace simple tokens with string values', () => { + const template = 'Hello, {{name}}!'; + const context: TemplateContext = { name: 'World' }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Hello, World!'); + }); + + it('should replace multiple tokens', () => { + const template = '{{greeting}}, {{name}}! Welcome to {{place}}.'; + const context: TemplateContext = { + greeting: 'Hello', + name: 'User', + place: 'ClankerContext', + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Hello, User! Welcome to ClankerContext.'); + }); + + it('should replace tokens with number values', () => { + const template = 'Count: {{count}}, Total: {{total}}'; + const context: TemplateContext = { count: 5, total: 100 }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Count: 5, Total: 100'); + }); + + it('should replace tokens with boolean values as strings', () => { + const template = 'Active: {{active}}, Enabled: {{enabled}}'; + const context: TemplateContext = { active: true, enabled: false }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Active: true, Enabled: false'); + }); + + it('should handle dotted token names (e.g., issue.name)', () => { + const template = 'Issue: {{issue.name}} ({{issue.type}})'; + const context: TemplateContext = { + 'issue.name': 'Login Bug', + 'issue.type': 'fix', + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Issue: Login Bug (fix)'); + }); + + it('should replace underscored token names', () => { + const template = 'Data: {{data_testid}}, Count: {{error_count}}'; + const context: TemplateContext = { + data_testid: 'btn-submit', + error_count: 3, + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Data: btn-submit, Count: 3'); + }); + + it('should remove unknown tokens', () => { + const template = 'Known: {{known}}, Unknown: {{unknown}}'; + const context: TemplateContext = { known: 'value' }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Known: value, Unknown: '); + }); + + it('should handle empty string values', () => { + const template = 'Value: [{{value}}]'; + const context: TemplateContext = { value: '' }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Value: []'); + }); + + it('should handle zero number values', () => { + const template = 'Count: {{count}}'; + const context: TemplateContext = { count: 0 }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Count: 0'); + }); + }); + + // ============================================================================ + // Section Conditionals (unchanged from v1.1.6) + // ============================================================================ + + describe('Section Conditionals', () => { + it('should render section when condition is true', () => { + const template = '{{#has_errors}}Errors found!{{/has_errors}}'; + const context: TemplateContext = { has_errors: true }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Errors found!'); + }); + + it('should hide section when condition is false', () => { + const template = '{{#has_errors}}Errors found!{{/has_errors}}'; + const context: TemplateContext = { has_errors: false }; + + const result = renderTemplate(template, context); + + expect(result).toBe(''); + }); + + it('should render section when condition is non-empty string', () => { + const template = '{{#message}}Message: {{message}}{{/message}}'; + const context: TemplateContext = { message: 'Hello' }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Message: Hello'); + }); + + it('should hide section when condition is empty string', () => { + const template = 'Before{{#message}}Message: {{message}}{{/message}}After'; + const context: TemplateContext = { message: '' }; + + const result = renderTemplate(template, context); + + expect(result).toBe('BeforeAfter'); + }); + + it('should render section when condition is positive number', () => { + const template = '{{#count}}Count: {{count}}{{/count}}'; + const context: TemplateContext = { count: 5 }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Count: 5'); + }); + + it('should hide section when condition is zero', () => { + const template = 'Start{{#count}}Count: {{count}}{{/count}}End'; + const context: TemplateContext = { count: 0 }; + + const result = renderTemplate(template, context); + + expect(result).toBe('StartEnd'); + }); + + it('should hide section when condition key is not in context', () => { + const template = 'Before{{#missing}}Content{{/missing}}After'; + const context: TemplateContext = {}; + + const result = renderTemplate(template, context); + + expect(result).toBe('BeforeAfter'); + }); + + it('should handle dotted section names', () => { + const template = '{{#issue.has_errors}}Has errors{{/issue.has_errors}}'; + const context: TemplateContext = { 'issue.has_errors': true }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Has errors'); + }); + + it('should handle multiple sections', () => { + const template = `{{#console_errors_present}}Console errors{{/console_errors_present}} +{{#network_errors_present}}Network errors{{/network_errors_present}}`; + const context: TemplateContext = { + console_errors_present: true, + network_errors_present: false, + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Console errors\n'); + }); + + it('should handle nested tokens inside sections', () => { + const template = `{{#console_errors_present}} +## Console Errors +**{{console_errors_count}} error(s) detected.** +{{console_errors_markdown}} +{{/console_errors_present}}`; + const context: TemplateContext = { + console_errors_present: true, + console_errors_count: 3, + console_errors_markdown: '### Error\n```\nTest error\n```', + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('## Console Errors'); + expect(result).toContain('**3 error(s) detected.**'); + expect(result).toContain('### Error'); + expect(result).toContain('Test error'); + }); + + it('should handle multiline section content', () => { + const template = `Header +{{#has_content}} +Line 1 +Line 2 +Line 3 +{{/has_content}} +Footer`; + const context: TemplateContext = { has_content: true }; + + const result = renderTemplate(template, context); + + expect(result).toContain('Header'); + expect(result).toContain('Line 1'); + expect(result).toContain('Line 2'); + expect(result).toContain('Line 3'); + expect(result).toContain('Footer'); + }); + }); + + // ============================================================================ + // NEW v1.1.8: Each Block Processing + // ============================================================================ + + describe('Each Block Processing', () => { + it('should iterate over array and render body for each item', () => { + const template = '{{#each items}}[{{element.name}}]{{/each}}'; + const context: TemplateContextWithArrays = { + items: [ + { name: 'Alice' }, + { name: 'Bob' }, + { name: 'Charlie' }, + ], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('[Alice][Bob][Charlie]'); + }); + + it('should produce no output for empty array', () => { + const template = 'Before{{#each items}}Item: {{element.name}}{{/each}}After'; + const context: TemplateContextWithArrays = { + items: [], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('BeforeAfter'); + }); + + it('should produce no output when array does not exist', () => { + const template = 'Before{{#each missing}}Content{{/each}}After'; + const context: TemplateContextWithArrays = {}; + + const result = renderTemplate(template, context); + + expect(result).toBe('BeforeAfter'); + }); + + it('should handle nested sections inside each block', () => { + const template = `{{#each elements}} +{{#element.has_info}}Info: {{element.info}}{{/element.has_info}} +{{/each}}`; + const context: TemplateContextWithArrays = { + elements: [ + { has_info: true, info: 'First' }, + { has_info: false, info: 'Hidden' }, + { has_info: true, info: 'Third' }, + ], + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('Info: First'); + expect(result).not.toContain('Hidden'); + expect(result).toContain('Info: Third'); + }); + + it('should require element. prefix for item tokens', () => { + // Tokens without element. prefix should not be replaced inside {{#each}} + const template = '{{#each items}}Name: {{name}}, Prefixed: {{element.name}}{{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ name: 'Test' }], + }; + + const result = renderTemplate(template, context); + + // {{name}} should remain empty/unreplaced, {{element.name}} should work + expect(result).toBe('Name: , Prefixed: Test'); + }); + + it('should handle multiple {{#each}} blocks', () => { + const template = `Users:{{#each users}}[{{element.name}}]{{/each}} +Items:{{#each items}}({{element.id}}){{/each}}`; + const context: TemplateContextWithArrays = { + users: [{ name: 'Alice' }, { name: 'Bob' }], + items: [{ id: '1' }, { id: '2' }, { id: '3' }], + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('[Alice][Bob]'); + expect(result).toContain('(1)(2)(3)'); + }); + + it('should handle single item array', () => { + const template = '{{#each items}}Only one: {{element.value}}{{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ value: 'single' }], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Only one: single'); + }); + + it('should handle multiline body in each block', () => { + const template = `{{#each elements}} +### Element +HTML: {{element.html}} +Selector: {{element.selector}} +{{/each}}`; + const context: TemplateContextWithArrays = { + elements: [ + { html: '', selector: '#btn' }, + { html: '', selector: '#input' }, + ], + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('### Element'); + expect(result).toContain('HTML: '); + expect(result).toContain('Selector: #btn'); + expect(result).toContain('HTML: '); + expect(result).toContain('Selector: #input'); + }); + }); + + // ============================================================================ + // NEW v1.1.8: Special Variables in Each Blocks + // ============================================================================ + + describe('Special Variables in Each Blocks', () => { + it('should replace @index with zero-based index', () => { + const template = '{{#each items}}{{@index}}:{{element.name}} {{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ name: 'A' }, { name: 'B' }, { name: 'C' }], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('0:A 1:B 2:C '); + }); + + it('should replace @number with one-based number', () => { + const template = '{{#each items}}#{{@number}}: {{element.name}}\n{{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ name: 'First' }, { name: 'Second' }, { name: 'Third' }], + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('#1: First'); + expect(result).toContain('#2: Second'); + expect(result).toContain('#3: Third'); + }); + + it('should replace @first with "true" only for first item', () => { + const template = '{{#each items}}[{{@first}}]{{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ val: 'a' }, { val: 'b' }, { val: 'c' }], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('[true][][]'); + }); + + it('should replace @last with "true" only for last item', () => { + const template = '{{#each items}}[{{@last}}]{{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ val: 'a' }, { val: 'b' }, { val: 'c' }], + }; + + const result = renderTemplate(template, context); + + // item 0 (first, not last): [], item 1 (middle): [], item 2 (last): [true] + expect(result).toBe('[][][true]'); + }); + + it('should replace @count with total array length', () => { + const template = '{{#each items}}({{@count}}){{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ val: 'a' }, { val: 'b' }, { val: 'c' }, { val: 'd' }], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('(4)(4)(4)(4)'); + }); + + it('should correctly identify first and last for single item', () => { + const template = '{{#each items}}first={{@first}}, last={{@last}}{{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ val: 'only' }], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('first=true, last=true'); + }); + + it('should correctly identify first and last for two items', () => { + const template = '{{#each items}}{{@number}}:f={{@first}},l={{@last}} {{/each}}'; + const context: TemplateContextWithArrays = { + items: [{ val: 'a' }, { val: 'b' }], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('1:f=true,l= 2:f=,l=true '); + }); + + it('should combine special variables with element tokens', () => { + const template = `{{#each elements}} +### Element {{@number}} of {{@count}} +Selector: {{element.selector}} +{{/each}}`; + const context: TemplateContextWithArrays = { + elements: [ + { selector: '#btn1' }, + { selector: '#btn2' }, + ], + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('### Element 1 of 2'); + expect(result).toContain('### Element 2 of 2'); + expect(result).toContain('Selector: #btn1'); + expect(result).toContain('Selector: #btn2'); + }); + }); + + // ============================================================================ + // Combined Token and Section Handling (unchanged from v1.1.6) + // ============================================================================ + + describe('Combined Token and Section Handling', () => { + it('should process sections before tokens', () => { + const template = `# Report +{{#errors_present}} +## Errors +Count: {{error_count}} +{{/errors_present}} +End`; + const context: TemplateContext = { + errors_present: true, + error_count: 5, + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('# Report'); + expect(result).toContain('## Errors'); + expect(result).toContain('Count: 5'); + expect(result).toContain('End'); + }); + + it('should remove section and its tokens when condition is false', () => { + const template = `Header +{{#errors_present}} +Count: {{error_count}} +Details: {{error_details}} +{{/errors_present}} +Footer`; + const context: TemplateContext = { + errors_present: false, + error_count: 5, + error_details: 'Some details', + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Header\n\nFooter'); + expect(result).not.toContain('Count:'); + expect(result).not.toContain('Details:'); + }); + + it('should handle complex template with multiple sections and tokens', () => { + const template = `# {{issue.type_title}} + +## Task +> {{issue.user_prompt}} + +**Page URL:** \`{{issue.page_url}}\` + +{{#console_errors_present}} +## Console Errors +**{{console_errors_count}} error(s)** +{{console_errors_markdown}} +{{/console_errors_present}} + +{{#network_errors_present}} +## Network Errors +**{{network_errors_count}} request(s) failed** +{{/network_errors_present}} + +## Summary +Type: {{issue.type}}`; + + const context: TemplateContext = { + 'issue.type_title': 'Bug Fix', + 'issue.user_prompt': 'Button not working', + 'issue.page_url': 'https://example.com', + 'issue.type': 'fix', + console_errors_present: true, + console_errors_count: 2, + console_errors_markdown: '### Error\nTest', + network_errors_present: false, + network_errors_count: 0, + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('# Bug Fix'); + expect(result).toContain('> Button not working'); + expect(result).toContain('`https://example.com`'); + expect(result).toContain('## Console Errors'); + expect(result).toContain('**2 error(s)**'); + expect(result).not.toContain('## Network Errors'); + expect(result).toContain('Type: fix'); + }); + + it('should process {{#each}} before global sections and tokens', () => { + const template = `Count: {{elements_count}} +{{#each elements}} +- {{element.name}} +{{/each}} +{{#has_footer}}Footer: {{footer_text}}{{/has_footer}}`; + const context: TemplateContextWithArrays = { + elements_count: 2, + elements: [{ name: 'First' }, { name: 'Second' }], + has_footer: true, + footer_text: 'The End', + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('Count: 2'); + expect(result).toContain('- First'); + expect(result).toContain('- Second'); + expect(result).toContain('Footer: The End'); + }); + }); + + // ============================================================================ + // Edge Cases + // ============================================================================ + + describe('Edge Cases', () => { + it('should handle empty template', () => { + const template = ''; + const context: TemplateContext = { foo: 'bar' }; + + const result = renderTemplate(template, context); + + expect(result).toBe(''); + }); + + it('should handle template with no tokens', () => { + const template = 'Just plain text without any tokens.'; + const context: TemplateContext = { foo: 'bar' }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Just plain text without any tokens.'); + }); + + it('should handle empty context', () => { + const template = 'Value: {{value}}'; + const context: TemplateContext = {}; + + const result = renderTemplate(template, context); + + expect(result).toBe('Value: '); + }); + + it('should handle special characters in values', () => { + const template = 'Code: {{code}}'; + const context: TemplateContext = { + code: '', + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Code: '); + }); + + it('should handle markdown in values', () => { + const template = '{{content}}'; + const context: TemplateContext = { + content: '# Header\n\n- List item\n- Another item\n\n```js\ncode\n```', + }; + + const result = renderTemplate(template, context); + + expect(result).toContain('# Header'); + expect(result).toContain('- List item'); + expect(result).toContain('```js'); + }); + + it('should handle values with curly braces', () => { + const template = 'Style: {{style}}'; + const context: TemplateContext = { + style: '{ color: red; }', + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Style: { color: red; }'); + }); + + it('should not recursively process replaced values', () => { + const template = 'Message: {{message}}'; + const context: TemplateContext = { + message: 'Use {{other}} for something else', + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Message: Use {{other}} for something else'); + }); + + it('should handle array with various value types', () => { + const template = '{{#each items}}{{element.val}}|{{/each}}'; + const context: TemplateContextWithArrays = { + items: [ + { val: 'string' }, + { val: 123 }, + { val: true }, + { val: '' }, + ], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('string|123|true||'); + }); + + it('should handle deeply nested element properties', () => { + const template = '{{#each items}}{{element.react.component_name}}|{{/each}}'; + const context: TemplateContextWithArrays = { + items: [ + { 'react.component_name': 'Button' }, + { 'react.component_name': 'Input' }, + ], + }; + + const result = renderTemplate(template, context); + + expect(result).toBe('Button|Input|'); + }); + }); +}); diff --git a/src/__tests__/templates/v1.1.8/__mocks__/testData.ts b/src/__tests__/templates/v1.1.8/__mocks__/testData.ts new file mode 100644 index 0000000..693e859 --- /dev/null +++ b/src/__tests__/templates/v1.1.8/__mocks__/testData.ts @@ -0,0 +1,664 @@ +/** + * Mock test data for v1.1.8 template tests. + * This file represents the data schema at version 1.1.8. + * DO NOT MODIFY once a new version folder is created. + * + * v1.1.8 adds: + * - React Source Extraction (component names, file locations, component stacks) + * - Quick Select Template (lightweight element capture) + * - {{#each}} Iteration with special variables (@index, @number, @first, @last, @count) + */ + +import type { CapturedElement, ConsoleError, Issue, NetworkError, ReactSourceInfo } from '@/shared/types'; + +// ============================================================================ +// React Source Mocks +// ============================================================================ + +/** + * Full React source info with all fields populated. + */ +export const mockReactSourceFull: ReactSourceInfo = { + componentName: 'LoginButton', + filePath: 'src/components/LoginButton.tsx', + lineNumber: 42, + columnNumber: 8, + componentStack: ['App', 'Layout', 'Header', 'LoginForm', 'LoginButton'], +}; + +/** + * React source that appears minified (short component names). + * Should be suppressed in template output. + */ +export const mockReactSourceMinified: ReactSourceInfo = { + componentName: 'n', + filePath: null, + lineNumber: null, + columnNumber: null, + componentStack: ['a', 'b', 'c', 'd', 'e'], // 3+ short names = minified +}; + +/** + * React source with only component name (no file/line info). + */ +export const mockReactSourceNameOnly: ReactSourceInfo = { + componentName: 'UserProfile', + filePath: null, + lineNumber: null, + columnNumber: null, + componentStack: ['App', 'Dashboard', 'UserProfile'], +}; + +/** + * React source with file path and line number but no column. + */ +export const mockReactSourcePartial: ReactSourceInfo = { + componentName: 'SubmitButton', + filePath: 'src/components/forms/SubmitButton.tsx', + lineNumber: 15, + columnNumber: null, + componentStack: ['App', 'Form', 'SubmitButton'], +}; + +// ============================================================================ +// Issue Mocks (from v1.1.6, extended with React source) +// ============================================================================ + +/** + * Mock Issue with all fields populated including React source. + * Type: 'fix' (bug fix scenario) + */ +export const mockFixIssue: Issue = { + id: 'issue-fix-123', + type: 'fix', + timestamp: 1706745600000, // 2024-02-01T00:00:00.000Z + name: 'Login Button Bug', + userPrompt: 'The login button does not respond when clicked', + elements: [ + { + html: '', + selector: '#login-btn', + customAttributes: [ + { + name: 'data-testid', + tokenName: 'data_testid', + value: 'login-button', + foundOn: 'selected', + }, + ], + }, + ], + pageUrl: 'https://example.com/login', +}; + +/** + * Mock Issue for enhancement scenario. + */ +export const mockEnhancementIssue: Issue = { + id: 'issue-enh-456', + type: 'enhancement', + timestamp: 1706745600000, + name: 'Add Dark Mode Toggle', + userPrompt: 'Add a dark mode toggle button to the header', + elements: [ + { + html: '', + selector: 'header.site-header', + customAttributes: [ + { + name: 'data-component', + tokenName: 'data_component', + value: 'main-header', + foundOn: 'selected', + }, + ], + }, + ], + pageUrl: 'https://example.com/dashboard', +}; + +/** + * Mock Issue with multiple elements. + */ +export const mockMultiElementIssue: Issue = { + id: 'issue-multi-789', + type: 'fix', + timestamp: 1706745600000, + name: 'Form Validation Issue', + userPrompt: 'Form fields are not validating correctly', + elements: [ + { + html: '', + selector: '#email', + customAttributes: [ + { + name: 'data-testid', + tokenName: 'data_testid', + value: 'email-input', + foundOn: 'selected', + }, + ], + }, + { + html: '', + selector: '#password', + customAttributes: [], + }, + { + html: '', + selector: 'button.submit-btn', + customAttributes: [], + }, + ], + pageUrl: 'https://example.com/signup', +}; + +/** + * Mock Issue with no custom attributes. + */ +export const mockIssueNoCustomAttrs: Issue = { + id: 'issue-noattr-101', + type: 'fix', + timestamp: 1706745600000, + name: 'Button Style Issue', + userPrompt: 'Button has wrong background color', + elements: [ + { + html: '', + selector: 'button.btn', + }, + ], + pageUrl: 'https://example.com/page', +}; + +/** + * Mock Issue with empty user prompt. + */ +export const mockIssueEmptyPrompt: Issue = { + id: 'issue-empty-102', + type: 'fix', + timestamp: 1706745600000, + name: 'Unknown Issue', + userPrompt: '', + elements: [ + { + html: '
Content
', + selector: 'div.widget', + }, + ], + pageUrl: 'https://example.com/widget', +}; + +/** + * Mock Issue with multiple custom attributes. + */ +export const mockIssueMultipleCustomAttrs: Issue = { + id: 'issue-multiattr-200', + type: 'fix', + timestamp: 1706745600000, + name: 'Complex Element Issue', + userPrompt: 'Element has interaction problems', + elements: [ + { + html: '
Content
', + selector: 'div.card', + customAttributes: [ + { + name: 'data-testid', + tokenName: 'data_testid', + value: 'main-card', + foundOn: 'selected', + }, + { + name: 'data-qa', + tokenName: 'data_qa', + value: 'card-123', + foundOn: 'selected', + }, + ], + }, + ], + pageUrl: 'https://example.com/cards', +}; + +// ============================================================================ +// NEW v1.1.8: Issues with React Source +// ============================================================================ + +/** + * Issue with full React source info on element. + */ +export const mockIssueWithReactSource: Issue = { + id: 'issue-react-301', + type: 'fix', + timestamp: 1706745600000, + name: 'React Component Bug', + userPrompt: 'The LoginButton component does not trigger onClick', + elements: [ + { + html: '', + selector: '#login-btn', + customAttributes: [ + { + name: 'data-testid', + tokenName: 'data_testid', + value: 'login-button', + foundOn: 'selected', + }, + ], + reactSource: mockReactSourceFull, + }, + ], + pageUrl: 'https://example.com/login', +}; + +/** + * Issue with multiple elements, some with React source, some without. + */ +export const mockIssueWithMixedReactSource: Issue = { + id: 'issue-mixed-react-302', + type: 'fix', + timestamp: 1706745600000, + name: 'Mixed Elements Issue', + userPrompt: 'Form submission not working', + elements: [ + { + html: '', + selector: '#email', + reactSource: { + componentName: 'EmailInput', + filePath: 'src/components/forms/EmailInput.tsx', + lineNumber: 23, + columnNumber: 4, + componentStack: ['App', 'Form', 'EmailInput'], + }, + }, + { + html: '', + selector: '#password', + // No React source - maybe a plain HTML element + }, + { + html: '', + selector: 'button.submit-btn', + reactSource: { + componentName: 'SubmitButton', + filePath: 'src/components/forms/SubmitButton.tsx', + lineNumber: 15, + columnNumber: 8, + componentStack: ['App', 'Form', 'SubmitButton'], + }, + }, + ], + pageUrl: 'https://example.com/signup', +}; + +/** + * Issue with minified React source (should be suppressed). + */ +export const mockIssueWithMinifiedReact: Issue = { + id: 'issue-minified-303', + type: 'fix', + timestamp: 1706745600000, + name: 'Minified React Bug', + userPrompt: 'Something is broken in production', + elements: [ + { + html: '', + selector: 'button.btn', + reactSource: mockReactSourceMinified, + }, + ], + pageUrl: 'https://example.com/prod', +}; + +/** + * Issue with React source that only has component name. + */ +export const mockIssueWithReactNameOnly: Issue = { + id: 'issue-react-name-304', + type: 'enhancement', + timestamp: 1706745600000, + name: 'Profile Enhancement', + userPrompt: 'Add avatar upload to user profile', + elements: [ + { + html: '
', + selector: '.profile-container', + reactSource: mockReactSourceNameOnly, + }, + ], + pageUrl: 'https://example.com/profile', +}; + +// ============================================================================ +// Console Error Mocks +// ============================================================================ + +/** + * Mock ConsoleError array with various error types. + */ +export const mockConsoleErrors: ConsoleError[] = [ + { + timestamp: 1706745600000, + message: 'Uncaught TypeError: Cannot read property \'click\' of undefined', + stackTrace: 'at handleClick (app.js:42:15)\n at HTMLButtonElement. (app.js:50:10)', + url: 'https://example.com/app.js', + lineNumber: 42, + }, + { + timestamp: 1706745601000, + message: 'Error: Network request failed', + stackTrace: 'at fetchData (api.js:15:5)\n at async loadUser (user.js:8:3)', + url: 'https://example.com/api.js', + lineNumber: 15, + }, +]; + +/** + * Single console error for simpler tests. + */ +export const mockSingleConsoleError: ConsoleError[] = [ + { + timestamp: 1706745600000, + message: 'Uncaught TypeError: Cannot read property \'click\' of undefined', + stackTrace: 'at handleClick (app.js:42:15)\n at HTMLButtonElement. (app.js:50:10)', + url: 'https://example.com/app.js', + lineNumber: 42, + }, +]; + +/** + * Console error without stack trace. + */ +export const mockConsoleErrorNoStack: ConsoleError[] = [ + { + timestamp: 1706745600000, + message: 'Script error.', + url: undefined, + lineNumber: undefined, + }, +]; + +/** + * Empty console errors array. + */ +export const mockEmptyConsoleErrors: ConsoleError[] = []; + +// ============================================================================ +// Network Error Mocks +// ============================================================================ + +/** + * Mock NetworkError array with various HTTP status codes. + */ +export const mockNetworkErrors: NetworkError[] = [ + { + timestamp: 1706745600000, + url: 'https://api.example.com/auth/login', + status: 500, + method: 'POST', + }, + { + timestamp: 1706745601000, + url: 'https://api.example.com/users/profile', + status: 404, + method: 'GET', + }, + { + timestamp: 1706745602000, + url: 'https://cdn.example.com/assets/missing.js', + status: 0, // CORS/Network error + method: 'GET', + }, +]; + +/** + * Single network error for simpler tests. + */ +export const mockSingleNetworkError: NetworkError[] = [ + { + timestamp: 1706745600000, + url: 'https://api.example.com/auth/login', + status: 500, + method: 'POST', + }, +]; + +/** + * Empty network errors array. + */ +export const mockEmptyNetworkErrors: NetworkError[] = []; + +// ============================================================================ +// Quick Select Mocks +// ============================================================================ + +/** + * Single element for quick select mode. + */ +export const mockQuickSelectSingleElement: CapturedElement[] = [ + { + html: '', + selector: '#submit-btn', + customAttributes: [ + { + name: 'data-testid', + tokenName: 'data_testid', + value: 'submit-button', + foundOn: 'selected', + }, + ], + reactSource: { + componentName: 'SubmitButton', + filePath: 'src/components/SubmitButton.tsx', + lineNumber: 28, + columnNumber: 6, + componentStack: ['App', 'Form', 'SubmitButton'], + }, + }, +]; + +/** + * Multiple elements for quick select mode. + */ +export const mockQuickSelectMultipleElements: CapturedElement[] = [ + { + html: '', + selector: '#username', + reactSource: { + componentName: 'UsernameInput', + filePath: 'src/components/forms/UsernameInput.tsx', + lineNumber: 12, + columnNumber: 4, + componentStack: ['App', 'LoginForm', 'UsernameInput'], + }, + }, + { + html: '', + selector: '#password', + reactSource: { + componentName: 'PasswordInput', + filePath: 'src/components/forms/PasswordInput.tsx', + lineNumber: 18, + columnNumber: 4, + componentStack: ['App', 'LoginForm', 'PasswordInput'], + }, + }, + { + html: '', + selector: 'button.login-btn', + reactSource: { + componentName: 'LoginButton', + filePath: 'src/components/forms/LoginButton.tsx', + lineNumber: 33, + columnNumber: 8, + componentStack: ['App', 'LoginForm', 'LoginButton'], + }, + }, +]; + +/** + * Quick select elements without React source. + */ +export const mockQuickSelectNoReact: CapturedElement[] = [ + { + html: 'About', + selector: 'a.nav-link', + }, + { + html: '', + selector: 'span.logo', + }, +]; + +// ============================================================================ +// Frozen v1.1.8 Templates - DO NOT MODIFY +// ============================================================================ + +/** + * v1.1.8 Fix template with {{#each elements}} iteration and React source support. + */ +export const V1_1_8_FIX_TEMPLATE = `# Bug Fix + +## Your Task +The user has identified a bug that needs fixing. Review the context below, identify the root cause, and implement a fix. + +## What the User Wants +> {{issue.user_prompt}} + +## Context +**Page URL:** \`{{issue.page_url}}\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request. These element(s) may be the source of the bug, or closely related to it. Inspect their attributes, event handlers, and parent/child relationships. + +{{#each elements}} +### Element {{@number}} + +\`\`\`html +{{element.html}} +\`\`\` + +**CSS Selector:** \`{{element.selector}}\` + +{{#element.react_source_present}} +**React Component:** \`{{element.react.component_name}}\` at \`{{element.react.file_location}}\` + +**Component Stack:** +{{element.react.component_stack}} +{{/element.react_source_present}} +{{/each}} +{{#console_errors_present}} +## Console Errors + +**{{console_errors_count}} error(s) detected.** These may indicate the root cause of the issue: + +{{console_errors_markdown}} +{{/console_errors_present}} + +{{#network_errors_present}} +## Failed Network Requests + +**{{network_errors_count}} failed request(s).** These may indicate API issues, missing resources, or server errors: + +| Status | Method | URL | +|--------|--------|-----| +{{network_errors_table}} +{{/network_errors_present}} + +## Summary + +**Type:** Bug Fix + +**Suggested approach:** +1. Review the error messages and stack traces for clues +2. Locate the target element and related code +3. Identify the root cause of the issue +4. Implement and test the fix +`; + +/** + * v1.1.8 Enhancement template with {{#each elements}} iteration. + */ +export const V1_1_8_ENHANCEMENT_TEMPLATE = `# Enhancement + +## Your Task +The user wants to add or change functionality on their web page. Review the context below and implement the requested enhancement. + +## What the User Wants +> {{issue.user_prompt}} + +## Context +**Page URL:** \`{{issue.page_url}}\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request. Use these element(s) as reference for where to apply the enhancement. You may need to modify these elements, their parents, or add sibling elements. + +{{#each elements}} +### Element {{@number}} + +\`\`\`html +{{element.html}} +\`\`\` + +**CSS Selector:** \`{{element.selector}}\` + +{{#element.react_source_present}} +**React Component:** \`{{element.react.component_name}}\` at \`{{element.react.file_location}}\` + +**Component Stack:** +{{element.react.component_stack}} +{{/element.react_source_present}} +{{/each}} +{{#console_errors_present}} +## Console Errors + +**{{console_errors_count}} error(s) detected.** These may indicate the root cause of the issue: + +{{console_errors_markdown}} +{{/console_errors_present}} + +{{#network_errors_present}} +## Failed Network Requests + +**{{network_errors_count}} failed request(s).** These may indicate API issues, missing resources, or server errors: + +| Status | Method | URL | +|--------|--------|-----| +{{network_errors_table}} +{{/network_errors_present}} + +## Summary + +**Type:** Enhancement + +**Suggested approach:** +1. Locate the target element in the codebase using the CSS selector +2. Understand the current behavior and surrounding code +3. Implement the requested enhancement +4. Test that existing functionality is not broken +`; + +/** + * v1.1.8 Quick Select template for lightweight element capture. + */ +export const V1_1_8_QUICK_SELECT_TEMPLATE = `{{#each elements}} +## Element {{@number}} + +\`\`\`html +{{element.html}} +\`\`\` + +**CSS Selector:** \`{{element.selector}}\` + +{{#element.react_source_present}} +**React Component:** \`{{element.react.component_name}}\` at \`{{element.react.file_location}}\` + +**Component Stack:** +{{element.react.component_stack}} +{{/element.react_source_present}} +{{/each}} +`; diff --git a/src/__tests__/templates/v1.1.8/__snapshots__/customTemplates.test.ts.snap b/src/__tests__/templates/v1.1.8/__snapshots__/customTemplates.test.ts.snap new file mode 100644 index 0000000..9f9115f --- /dev/null +++ b/src/__tests__/templates/v1.1.8/__snapshots__/customTemplates.test.ts.snap @@ -0,0 +1,668 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Custom Templates v1.1.8 - Kitchen Sink Snapshot Tests should match snapshot for enhancement issue: kitchen-sink-enhancement 1`] = ` +"# Enhancement Report + +## Issue Metadata +- **ID:** issue-enh-456 +- **Name:** Add Dark Mode Toggle +- **Type:** enhancement +- **Label:** Modify +- **Title:** Enhancement +- **Page URL:** https://example.com/dashboard +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** Add a dark mode toggle button to the header + +**Blockquote format:** +> Add a dark mode toggle button to the header + +## Elements Overview +- **Count:** 1 element(s) +- **First element selector:** header.site-header + +### First Element (Code Block - Legacy) +\`\`\`html + +\`\`\` +**CSS Selector:** \`header.site-header\` + +### First Element HTML (Plain - Legacy) +\`\`\`html + +\`\`\` + +### All Elements (Legacy Markdown) +\`\`\`html + +\`\`\` + +**CSS Selector:** \`header.site-header\` + +### All Elements (Each Iteration) + +#### Element 1 of 1 +- **Index:** 0 +- **First:** true +- **Last:** true + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`header.site-header\` + + + + + + + + + + + + + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; + +exports[`Custom Templates v1.1.8 - Kitchen Sink Snapshot Tests should match snapshot for kitchen sink with React source and all errors: kitchen-sink-react-all-errors 1`] = ` +"# Bug Fix Report + +## Issue Metadata +- **ID:** issue-react-301 +- **Name:** React Component Bug +- **Type:** fix +- **Label:** Fix +- **Title:** Bug Fix +- **Page URL:** https://example.com/login +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** The LoginButton component does not trigger onClick + +**Blockquote format:** +> The LoginButton component does not trigger onClick + +## Elements Overview +- **Count:** 1 element(s) +- **First element selector:** #login-btn + +### First Element (Code Block - Legacy) +\`\`\`html + +\`\`\` +**CSS Selector:** \`#login-btn\` + +### First Element HTML (Plain - Legacy) +\`\`\`html + +\`\`\` + +### All Elements (Legacy Markdown) +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + +### All Elements (Each Iteration) + +#### Element 1 of 1 +- **Index:** 0 +- **First:** true +- **Last:** true + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + + +**React Component:** \`LoginButton\` +**File Path:** src/components/LoginButton.tsx +**Line:** 42 +**Column:** 8 +**Location:** src/components/LoginButton.tsx:42:8 +**Stack:** + → App + → Layout + → Header + → LoginForm + → LoginButton + + + +**Data Test ID:** login-button + + + + +## Console Errors +**2 error(s) detected** + +### Error +\`\`\` +Uncaught TypeError: Cannot read property 'click' of undefined +\`\`\` + +**Stack trace:** +\`\`\` +at handleClick (app.js:42:15) + at HTMLButtonElement. (app.js:50:10) +\`\`\` +**Source:** \`https://example.com/app.js:42\` + +### Error +\`\`\` +Error: Network request failed +\`\`\` + +**Stack trace:** +\`\`\` +at fetchData (api.js:15:5) + at async loadUser (user.js:8:3) +\`\`\` +**Source:** \`https://example.com/api.js:15\` + + + +## Network Errors +**3 failed request(s)** + +| Status | Method | URL | +|--------|--------|-----| +| 500 | POST | \`https://api.example.com/auth/login\` | +| 404 | GET | \`https://api.example.com/users/profile\` | +| CORS/Network | GET | \`https://cdn.example.com/assets/missing.js\` | + + + +## Error Summary +This issue has associated errors that may help diagnose the problem. + + + +## Global Custom Attribute: data-testid +**Value:** login-button + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; + +exports[`Custom Templates v1.1.8 - Kitchen Sink Snapshot Tests should match snapshot for kitchen sink with mixed React source: kitchen-sink-mixed-react 1`] = ` +"# Bug Fix Report + +## Issue Metadata +- **ID:** issue-mixed-react-302 +- **Name:** Mixed Elements Issue +- **Type:** fix +- **Label:** Fix +- **Title:** Bug Fix +- **Page URL:** https://example.com/signup +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** Form submission not working + +**Blockquote format:** +> Form submission not working + +## Elements Overview +- **Count:** 3 element(s) +- **First element selector:** #email + +### First Element (Code Block - Legacy) +\`\`\`html + +\`\`\` +**CSS Selector:** \`#email\` + +### First Element HTML (Plain - Legacy) +\`\`\`html + +\`\`\` + +### All Elements (Legacy Markdown) +### Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#email\` + +### Element 2 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#password\` + +### Element 3 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.submit-btn\` + +### All Elements (Each Iteration) + +#### Element 1 of 3 +- **Index:** 0 +- **First:** true +- **Last:** + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#email\` + + +**React Component:** \`EmailInput\` +**File Path:** src/components/forms/EmailInput.tsx +**Line:** 23 +**Column:** 4 +**Location:** src/components/forms/EmailInput.tsx:23:4 +**Stack:** + → App + → Form + → EmailInput + + + + +#### Element 2 of 3 +- **Index:** 1 +- **First:** +- **Last:** + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#password\` + + + + + +#### Element 3 of 3 +- **Index:** 2 +- **First:** +- **Last:** true + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.submit-btn\` + + +**React Component:** \`SubmitButton\` +**File Path:** src/components/forms/SubmitButton.tsx +**Line:** 15 +**Column:** 8 +**Location:** src/components/forms/SubmitButton.tsx:15:8 +**Stack:** + → App + → Form + → SubmitButton + + + + + + + + + + + + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; + +exports[`Custom Templates v1.1.8 - Kitchen Sink Snapshot Tests should match snapshot for kitchen sink with multiple elements: kitchen-sink-multi-elements 1`] = ` +"# Bug Fix Report + +## Issue Metadata +- **ID:** issue-multi-789 +- **Name:** Form Validation Issue +- **Type:** fix +- **Label:** Fix +- **Title:** Bug Fix +- **Page URL:** https://example.com/signup +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** Form fields are not validating correctly + +**Blockquote format:** +> Form fields are not validating correctly + +## Elements Overview +- **Count:** 3 element(s) +- **First element selector:** #email + +### First Element (Code Block - Legacy) +\`\`\`html + +\`\`\` +**CSS Selector:** \`#email\` + +### First Element HTML (Plain - Legacy) +\`\`\`html + +\`\`\` + +### All Elements (Legacy Markdown) +### Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#email\` + +### Element 2 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#password\` + +### Element 3 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.submit-btn\` + +### All Elements (Each Iteration) + +#### Element 1 of 3 +- **Index:** 0 +- **First:** true +- **Last:** + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#email\` + + + + +**Data Test ID:** email-input + + +#### Element 2 of 3 +- **Index:** 1 +- **First:** +- **Last:** + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#password\` + + + + + +#### Element 3 of 3 +- **Index:** 2 +- **First:** +- **Last:** true + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.submit-btn\` + + + + + + + +## Console Errors +**2 error(s) detected** + +### Error +\`\`\` +Uncaught TypeError: Cannot read property 'click' of undefined +\`\`\` + +**Stack trace:** +\`\`\` +at handleClick (app.js:42:15) + at HTMLButtonElement. (app.js:50:10) +\`\`\` +**Source:** \`https://example.com/app.js:42\` + +### Error +\`\`\` +Error: Network request failed +\`\`\` + +**Stack trace:** +\`\`\` +at fetchData (api.js:15:5) + at async loadUser (user.js:8:3) +\`\`\` +**Source:** \`https://example.com/api.js:15\` + + + +## Network Errors +**3 failed request(s)** + +| Status | Method | URL | +|--------|--------|-----| +| 500 | POST | \`https://api.example.com/auth/login\` | +| 404 | GET | \`https://api.example.com/users/profile\` | +| CORS/Network | GET | \`https://cdn.example.com/assets/missing.js\` | + + + +## Error Summary +This issue has associated errors that may help diagnose the problem. + + + +## Global Custom Attribute: data-testid +**Value:** email-input + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; + +exports[`Custom Templates v1.1.8 - Kitchen Sink Snapshot Tests should match snapshot for kitchen sink with no errors: kitchen-sink-no-errors 1`] = ` +"# Bug Fix Report + +## Issue Metadata +- **ID:** issue-fix-123 +- **Name:** Login Button Bug +- **Type:** fix +- **Label:** Fix +- **Title:** Bug Fix +- **Page URL:** https://example.com/login +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** The login button does not respond when clicked + +**Blockquote format:** +> The login button does not respond when clicked + +## Elements Overview +- **Count:** 1 element(s) +- **First element selector:** #login-btn + +### First Element (Code Block - Legacy) +\`\`\`html + +\`\`\` +**CSS Selector:** \`#login-btn\` + +### First Element HTML (Plain - Legacy) +\`\`\`html + +\`\`\` + +### All Elements (Legacy Markdown) +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + +### All Elements (Each Iteration) + +#### Element 1 of 1 +- **Index:** 0 +- **First:** true +- **Last:** true + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + + + + +**Data Test ID:** login-button + + + + + + + + + + +## Global Custom Attribute: data-testid +**Value:** login-button + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; + +exports[`Custom Templates v1.1.8 - Kitchen Sink Snapshot Tests should match snapshot for minified React (suppressed): kitchen-sink-minified-react 1`] = ` +"# Bug Fix Report + +## Issue Metadata +- **ID:** issue-minified-303 +- **Name:** Minified React Bug +- **Type:** fix +- **Label:** Fix +- **Title:** Bug Fix +- **Page URL:** https://example.com/prod +- **Timestamp:** 2024-02-01T00:00:00.000Z + +## User Request +**Raw prompt:** Something is broken in production + +**Blockquote format:** +> Something is broken in production + +## Elements Overview +- **Count:** 1 element(s) +- **First element selector:** button.btn + +### First Element (Code Block - Legacy) +\`\`\`html + +\`\`\` +**CSS Selector:** \`button.btn\` + +### First Element HTML (Plain - Legacy) +\`\`\`html + +\`\`\` + +### All Elements (Legacy Markdown) +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.btn\` + +### All Elements (Each Iteration) + +#### Element 1 of 1 +- **Index:** 0 +- **First:** true +- **Last:** true + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.btn\` + + + + + + + + + + + + + + + + +--- +*Generated at 2024-02-01T00:00:00.000Z* +" +`; diff --git a/src/__tests__/templates/v1.1.8/__snapshots__/defaultTemplates.test.ts.snap b/src/__tests__/templates/v1.1.8/__snapshots__/defaultTemplates.test.ts.snap new file mode 100644 index 0000000..6365379 --- /dev/null +++ b/src/__tests__/templates/v1.1.8/__snapshots__/defaultTemplates.test.ts.snap @@ -0,0 +1,320 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Default Templates v1.1.8 Snapshot Tests should match snapshot for enhancement template: enhancement-template 1`] = ` +"# Enhancement + +## Your Task +The user wants to add or change functionality on their web page. Review the context below and implement the requested enhancement. + +## What the User Wants +> Add a dark mode toggle button to the header + +## Context +**Page URL:** \`https://example.com/dashboard\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request. Use these element(s) as reference for where to apply the enhancement. You may need to modify these elements, their parents, or add sibling elements. + + +### Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`header.site-header\` + + + + + + + +## Summary + +**Type:** Enhancement + +**Suggested approach:** +1. Locate the target element in the codebase using the CSS selector +2. Understand the current behavior and surrounding code +3. Implement the requested enhancement +4. Test that existing functionality is not broken +" +`; + +exports[`Default Templates v1.1.8 Snapshot Tests should match snapshot for fix template with React source: fix-template-with-react-source 1`] = ` +"# Bug Fix + +## Your Task +The user has identified a bug that needs fixing. Review the context below, identify the root cause, and implement a fix. + +## What the User Wants +> The LoginButton component does not trigger onClick + +## Context +**Page URL:** \`https://example.com/login\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request. These element(s) may be the source of the bug, or closely related to it. Inspect their attributes, event handlers, and parent/child relationships. + + +### Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + + +**React Component:** \`LoginButton\` at \`src/components/LoginButton.tsx:42:8\` + +**Component Stack:** + → App + → Layout + → Header + → LoginForm + → LoginButton + + + +## Console Errors + +**1 error(s) detected.** These may indicate the root cause of the issue: + +### Error +\`\`\` +Uncaught TypeError: Cannot read property 'click' of undefined +\`\`\` + +**Stack trace:** +\`\`\` +at handleClick (app.js:42:15) + at HTMLButtonElement. (app.js:50:10) +\`\`\` +**Source:** \`https://example.com/app.js:42\` + + + +## Failed Network Requests + +**1 failed request(s).** These may indicate API issues, missing resources, or server errors: + +| Status | Method | URL | +|--------|--------|-----| +| 500 | POST | \`https://api.example.com/auth/login\` | + + +## Summary + +**Type:** Bug Fix + +**Suggested approach:** +1. Review the error messages and stack traces for clues +2. Locate the target element and related code +3. Identify the root cause of the issue +4. Implement and test the fix +" +`; + +exports[`Default Templates v1.1.8 Snapshot Tests should match snapshot for fix template with multiple elements: fix-template-multiple-elements-mixed-react 1`] = ` +"# Bug Fix + +## Your Task +The user has identified a bug that needs fixing. Review the context below, identify the root cause, and implement a fix. + +## What the User Wants +> Form submission not working + +## Context +**Page URL:** \`https://example.com/signup\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request. These element(s) may be the source of the bug, or closely related to it. Inspect their attributes, event handlers, and parent/child relationships. + + +### Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#email\` + + +**React Component:** \`EmailInput\` at \`src/components/forms/EmailInput.tsx:23:4\` + +**Component Stack:** + → App + → Form + → EmailInput + + +### Element 2 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#password\` + + + +### Element 3 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.submit-btn\` + + +**React Component:** \`SubmitButton\` at \`src/components/forms/SubmitButton.tsx:15:8\` + +**Component Stack:** + → App + → Form + → SubmitButton + + + + + + +## Summary + +**Type:** Bug Fix + +**Suggested approach:** +1. Review the error messages and stack traces for clues +2. Locate the target element and related code +3. Identify the root cause of the issue +4. Implement and test the fix +" +`; + +exports[`Default Templates v1.1.8 Snapshot Tests should match snapshot for fix template without React source: fix-template-without-react-source 1`] = ` +"# Bug Fix + +## Your Task +The user has identified a bug that needs fixing. Review the context below, identify the root cause, and implement a fix. + +## What the User Wants +> The login button does not respond when clicked + +## Context +**Page URL:** \`https://example.com/login\` + +## Target Element(s) + +The user selected the following element(s) as the focus of their request. These element(s) may be the source of the bug, or closely related to it. Inspect their attributes, event handlers, and parent/child relationships. + + +### Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#login-btn\` + + + + + + + +## Summary + +**Type:** Bug Fix + +**Suggested approach:** +1. Review the error messages and stack traces for clues +2. Locate the target element and related code +3. Identify the root cause of the issue +4. Implement and test the fix +" +`; + +exports[`Default Templates v1.1.8 Snapshot Tests should match snapshot for quick select multiple elements: quick-select-multiple-elements 1`] = ` +" +## Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#username\` + + +**React Component:** \`UsernameInput\` at \`src/components/forms/UsernameInput.tsx:12:4\` + +**Component Stack:** + → App + → LoginForm + → UsernameInput + + +## Element 2 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#password\` + + +**React Component:** \`PasswordInput\` at \`src/components/forms/PasswordInput.tsx:18:4\` + +**Component Stack:** + → App + → LoginForm + → PasswordInput + + +## Element 3 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.login-btn\` + + +**React Component:** \`LoginButton\` at \`src/components/forms/LoginButton.tsx:33:8\` + +**Component Stack:** + → App + → LoginForm + → LoginButton + + +" +`; + +exports[`Default Templates v1.1.8 Snapshot Tests should match snapshot for quick select single element: quick-select-single-element 1`] = ` +" +## Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#submit-btn\` + + +**React Component:** \`SubmitButton\` at \`src/components/SubmitButton.tsx:28:6\` + +**Component Stack:** + → App + → Form + → SubmitButton + + +" +`; diff --git a/src/__tests__/templates/v1.1.8/__snapshots__/quickSelect.test.ts.snap b/src/__tests__/templates/v1.1.8/__snapshots__/quickSelect.test.ts.snap new file mode 100644 index 0000000..289f54f --- /dev/null +++ b/src/__tests__/templates/v1.1.8/__snapshots__/quickSelect.test.ts.snap @@ -0,0 +1,104 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Quick Select Templates v1.1.8 Snapshot Tests should match snapshot for elements without React source: quick-select-no-react 1`] = ` +" +## Element 1 + +\`\`\`html +About +\`\`\` + +**CSS Selector:** \`a.nav-link\` + + + +## Element 2 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`span.logo\` + + + +" +`; + +exports[`Quick Select Templates v1.1.8 Snapshot Tests should match snapshot for multiple elements with React source: quick-select-multiple-with-react 1`] = ` +" +## Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#username\` + + +**React Component:** \`UsernameInput\` at \`src/components/forms/UsernameInput.tsx:12:4\` + +**Component Stack:** + → App + → LoginForm + → UsernameInput + + +## Element 2 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#password\` + + +**React Component:** \`PasswordInput\` at \`src/components/forms/PasswordInput.tsx:18:4\` + +**Component Stack:** + → App + → LoginForm + → PasswordInput + + +## Element 3 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`button.login-btn\` + + +**React Component:** \`LoginButton\` at \`src/components/forms/LoginButton.tsx:33:8\` + +**Component Stack:** + → App + → LoginForm + → LoginButton + + +" +`; + +exports[`Quick Select Templates v1.1.8 Snapshot Tests should match snapshot for single element with React source: quick-select-single-with-react 1`] = ` +" +## Element 1 + +\`\`\`html + +\`\`\` + +**CSS Selector:** \`#submit-btn\` + + +**React Component:** \`SubmitButton\` at \`src/components/SubmitButton.tsx:28:6\` + +**Component Stack:** + → App + → Form + → SubmitButton + + +" +`; diff --git a/src/__tests__/templates/v1.1.8/customTemplates.test.ts b/src/__tests__/templates/v1.1.8/customTemplates.test.ts new file mode 100644 index 0000000..0319361 --- /dev/null +++ b/src/__tests__/templates/v1.1.8/customTemplates.test.ts @@ -0,0 +1,793 @@ +/** + * v1.1.8 - Custom Template Tests (Kitchen Sink) + * + * Tests a custom template using ALL 31+ available tags at v1.1.8. + * This ensures backward compatibility as the template system evolves. + * + * When new versions are created, this file should NOT be modified. + * Instead, create a new version folder with updated tests. + * + * Breaking Change Detection: + * - All version tests run against the CURRENT renderTemplate() code + * - If a tag is renamed/removed, these tests will FAIL + * - The test "all tokens render correctly" catches leftover {{...}} tokens + * + * v1.1.8 Tags (31+ tags): + * + * Issue Metadata (9 tags): + * - {{issue.id}}, {{issue.name}}, {{issue.type}} + * - {{issue.type_label}}, {{issue.type_title}} + * - {{issue.page_url}}, {{issue.user_prompt}} + * - {{issue.user_prompt_blockquote}}, {{issue.timestamp_iso}} + * + * Legacy Elements (8 tags): + * - {{elements_count}}, {{elements_markdown}} + * - {{elements_html_markdown}}, {{elements_selectors_markdown}} + * - {{elements_html_first}}, {{elements_selector_first}} + * - {{element.html}}, {{element.css_selector}} + * + * Each Iteration (7 tags): + * - {{#each elements}}...{{/each}} + * - {{@index}}, {{@number}}, {{@first}}, {{@last}}, {{@count}} + * + * React Source in Each (7 tags): + * - {{#element.react_source_present}}...{{/element.react_source_present}} + * - {{element.react.component_name}}, {{element.react.file_path}} + * - {{element.react.line_number}}, {{element.react.column_number}} + * - {{element.react.file_location}}, {{element.react.component_stack}} + * + * Errors (7 tags): + * - {{console_errors_count}}, {{#console_errors_present}}, {{console_errors_markdown}} + * - {{network_errors_count}}, {{#network_errors_present}}, {{network_errors_table}} + * - {{#errors_present}} + * + * Custom Attributes (dynamic): + * - {{data_testid}}, {{#data_testid_present}} + */ + +import { renderTemplate, type TemplateContextWithArrays, type TemplateArrayItem } from '@/exporter/PromptTemplateRenderer'; +import { + mockFixIssue, + mockEnhancementIssue, + mockMultiElementIssue, + mockIssueNoCustomAttrs, + mockIssueEmptyPrompt, + mockIssueMultipleCustomAttrs, + mockIssueWithReactSource, + mockIssueWithMixedReactSource, + mockIssueWithMinifiedReact, + mockConsoleErrors, + mockEmptyConsoleErrors, + mockNetworkErrors, + mockEmptyNetworkErrors, +} from './__mocks__/testData'; +import type { Issue, ConsoleError, NetworkError, CapturedElement, ReactSourceInfo } from '@/shared/types'; + +/** + * Kitchen sink template using ALL 31+ tags available at v1.1.8. + */ +const KITCHEN_SINK_TEMPLATE = `# {{issue.type_title}} Report + +## Issue Metadata +- **ID:** {{issue.id}} +- **Name:** {{issue.name}} +- **Type:** {{issue.type}} +- **Label:** {{issue.type_label}} +- **Title:** {{issue.type_title}} +- **Page URL:** {{issue.page_url}} +- **Timestamp:** {{issue.timestamp_iso}} + +## User Request +**Raw prompt:** {{issue.user_prompt}} + +**Blockquote format:** +{{issue.user_prompt_blockquote}} + +## Elements Overview +- **Count:** {{elements_count}} element(s) +- **First element selector:** {{elements_selector_first}} + +### First Element (Code Block - Legacy) +{{element.html}} +{{element.css_selector}} + +### First Element HTML (Plain - Legacy) +\`\`\`html +{{elements_html_first}} +\`\`\` + +### All Elements (Legacy Markdown) +{{elements_markdown}} + +### All Elements (Each Iteration) +{{#each elements}} +#### Element {{@number}} of {{@count}} +- **Index:** {{@index}} +- **First:** {{@first}} +- **Last:** {{@last}} + +\`\`\`html +{{element.html}} +\`\`\` + +**CSS Selector:** \`{{element.selector}}\` + +{{#element.react_source_present}} +**React Component:** \`{{element.react.component_name}}\` +**File Path:** {{element.react.file_path}} +**Line:** {{element.react.line_number}} +**Column:** {{element.react.column_number}} +**Location:** {{element.react.file_location}} +**Stack:** +{{element.react.component_stack}} +{{/element.react_source_present}} + +{{#element.data_testid_present}} +**Data Test ID:** {{element.data_testid}} +{{/element.data_testid_present}} +{{/each}} + +{{#console_errors_present}} +## Console Errors +**{{console_errors_count}} error(s) detected** + +{{console_errors_markdown}} +{{/console_errors_present}} + +{{#network_errors_present}} +## Network Errors +**{{network_errors_count}} failed request(s)** + +| Status | Method | URL | +|--------|--------|-----| +{{network_errors_table}} +{{/network_errors_present}} + +{{#errors_present}} +## Error Summary +This issue has associated errors that may help diagnose the problem. +{{/errors_present}} + +{{#data_testid_present}} +## Global Custom Attribute: data-testid +**Value:** {{data_testid}} +{{/data_testid_present}} + +{{#data_qa_present}} +## Global Custom Attribute: data-qa +**Value:** {{data_qa}} +{{/data_qa_present}} + +--- +*Generated at {{issue.timestamp_iso}}* +`; + +// ============================================================================ +// Helper Functions (same as defaultTemplates.test.ts) +// ============================================================================ + +function isLikelyMinified(reactSource: ReactSourceInfo): boolean { + const shortNameCount = reactSource.componentStack.filter( + (name) => name.length <= 2 + ).length; + const mainNameShort = reactSource.componentName !== null && reactSource.componentName.length <= 2; + const totalShort = shortNameCount + (mainNameShort ? 1 : 0); + return totalShort >= 3; +} + +function buildSingleElementReactTokens(element: CapturedElement): Record { + const reactSource = element.reactSource; + + if (!reactSource || isLikelyMinified(reactSource)) { + return { + react_source_present: false, + 'react.component_name': '', + 'react.file_path': '', + 'react.line_number': '', + 'react.column_number': '', + 'react.component_stack': '', + 'react.file_location': '', + }; + } + + let fileLocation = ''; + if (reactSource.filePath) { + fileLocation = reactSource.filePath; + if (reactSource.lineNumber) { + fileLocation += `:${reactSource.lineNumber}`; + if (reactSource.columnNumber) { + fileLocation += `:${reactSource.columnNumber}`; + } + } + } + + const componentStackStr = reactSource.componentStack.length > 0 + ? reactSource.componentStack.map((name) => ` → ${name}`).join('\n') + : ''; + + return { + react_source_present: true, + 'react.component_name': reactSource.componentName || '', + 'react.file_path': reactSource.filePath || '', + 'react.line_number': reactSource.lineNumber?.toString() || '', + 'react.column_number': reactSource.columnNumber?.toString() || '', + 'react.component_stack': componentStackStr, + 'react.file_location': fileLocation, + }; +} + +function buildSingleElementCustomAttributes(element: CapturedElement): Record { + const tokens: Record = {}; + if (element.customAttributes) { + for (const attr of element.customAttributes) { + tokens[attr.tokenName] = attr.value; + tokens[`${attr.tokenName}_present`] = true; + } + } + return tokens; +} + +function buildElementsArray(elements: CapturedElement[]): TemplateArrayItem[] { + return elements.map((element) => { + const reactTokens = buildSingleElementReactTokens(element); + const customAttrTokens = buildSingleElementCustomAttributes(element); + + return { + html: formatHTML(element.html), + html_raw: element.html, + selector: element.selector, + ...reactTokens, + ...customAttrTokens, + }; + }); +} + +function formatHTML(html: string): string { + let formatted = html.replace(/>\n<').replace(/>\s+\n<'); + if (formatted.length > 5000) { + formatted = formatted.substring(0, 5000) + '\n'; + } + return formatted; +} + +function buildElementsMarkdown(elements: CapturedElement[]): string { + if (elements.length === 0) return ''; + const lines: string[] = []; + if (elements.length === 1) { + lines.push('```html'); + lines.push(formatHTML(elements[0].html)); + lines.push('```'); + lines.push(''); + lines.push(`**CSS Selector:** \`${elements[0].selector}\``); + } else { + elements.forEach((element, index) => { + lines.push(`### Element ${index + 1}`); + lines.push(''); + lines.push('```html'); + lines.push(formatHTML(element.html)); + lines.push('```'); + lines.push(''); + lines.push(`**CSS Selector:** \`${element.selector}\``); + if (index < elements.length - 1) { + lines.push(''); + } + }); + } + return lines.join('\n').trimEnd(); +} + +function buildElementsHtmlMarkdown(elements: CapturedElement[]): string { + if (elements.length === 0) return ''; + const lines: string[] = []; + if (elements.length === 1) { + lines.push('```html'); + lines.push(formatHTML(elements[0].html)); + lines.push('```'); + } else { + elements.forEach((element, index) => { + lines.push(`### Element ${index + 1}`); + lines.push(''); + lines.push('```html'); + lines.push(formatHTML(element.html)); + lines.push('```'); + if (index < elements.length - 1) { + lines.push(''); + } + }); + } + return lines.join('\n').trimEnd(); +} + +function buildElementsSelectorsMarkdown(elements: CapturedElement[]): string { + if (elements.length === 0) return ''; + if (elements.length === 1) { + return `**CSS Selector:** \`${elements[0].selector}\``; + } + return elements.map((element, index) => `- Element ${index + 1}: \`${element.selector}\``).join('\n'); +} + +function truncate(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return str.substring(0, maxLen - 3) + '...'; +} + +function buildConsoleErrorsMarkdown(consoleErrors: ConsoleError[]): string { + if (consoleErrors.length === 0) return ''; + const lines: string[] = []; + const errorsToShow = consoleErrors.slice(0, 15); + errorsToShow.forEach((error, index) => { + lines.push('### Error'); + lines.push('```'); + lines.push(truncate(error.message, 500)); + lines.push('```'); + if (error.stackTrace) { + lines.push(''); + lines.push('**Stack trace:**'); + lines.push('```'); + const stackLines = error.stackTrace.split('\n').slice(0, 5); + lines.push(stackLines.join('\n')); + lines.push('```'); + } + if (error.url) { + lines.push(`**Source:** \`${error.url}${error.lineNumber ? `:${error.lineNumber}` : ''}\``); + } + if (index < errorsToShow.length - 1) { + lines.push(''); + } + }); + return lines.join('\n').trimEnd(); +} + +function buildNetworkErrorsTable(networkErrors: NetworkError[]): string { + if (networkErrors.length === 0) return ''; + const errorsToShow = networkErrors.slice(0, 15); + const lines = errorsToShow.map((error) => { + const shortUrl = truncate(error.url, 80); + const status = error.status === 0 ? 'CORS/Network' : error.status.toString(); + return `| ${status} | ${error.method} | \`${shortUrl}\` |`; + }); + return lines.join('\n'); +} + +function buildCustomAttributeTokens(elements: CapturedElement[]): Record { + const customAttrMap = new Map(); + for (const element of elements) { + if (element.customAttributes) { + for (const attr of element.customAttributes) { + if (!customAttrMap.has(attr.tokenName)) { + customAttrMap.set(attr.tokenName, attr.value); + } + } + } + } + const tokens: Record = {}; + for (const [tokenName, value] of customAttrMap) { + tokens[tokenName] = value; + tokens[`${tokenName}_present`] = true; + } + return tokens; +} + +function buildTestContext( + issue: Issue, + consoleErrors: ConsoleError[], + networkErrors: NetworkError[] +): TemplateContextWithArrays { + const isEnhancement = issue.type === 'enhancement'; + const issueName = issue.name || 'Untitled'; + const hasErrors = consoleErrors.length > 0 || networkErrors.length > 0; + + const userPromptBlockquote = issue.userPrompt + ? `> ${issue.userPrompt.split('\n').join('\n> ')}` + : '_No description provided. Examine the selected element and errors for context._'; + + const firstElement = issue.elements[0]; + const elementHtml = firstElement + ? `\`\`\`html\n${formatHTML(firstElement.html)}\n\`\`\`` + : ''; + const elementCssSelector = firstElement + ? `**CSS Selector:** \`${firstElement.selector}\`` + : ''; + + return { + 'issue.id': issue.id, + 'issue.name': issueName, + 'issue.type': issue.type, + 'issue.type_label': isEnhancement ? 'Modify' : 'Fix', + 'issue.type_title': isEnhancement ? 'Enhancement' : 'Bug Fix', + 'issue.page_url': issue.pageUrl, + 'issue.user_prompt': issue.userPrompt || '', + 'issue.user_prompt_blockquote': userPromptBlockquote, + 'issue.timestamp_iso': new Date(issue.timestamp).toISOString(), + elements_count: issue.elements.length, + elements_markdown: buildElementsMarkdown(issue.elements), + elements_html_markdown: buildElementsHtmlMarkdown(issue.elements), + elements_selectors_markdown: buildElementsSelectorsMarkdown(issue.elements), + elements_html_first: issue.elements[0] ? formatHTML(issue.elements[0].html) : '', + elements_selector_first: issue.elements[0]?.selector || '', + 'element.html': elementHtml, + 'element.css_selector': elementCssSelector, + console_errors_count: consoleErrors.length, + console_errors_present: consoleErrors.length > 0, + console_errors_markdown: buildConsoleErrorsMarkdown(consoleErrors), + network_errors_count: networkErrors.length, + network_errors_present: networkErrors.length > 0, + network_errors_table: buildNetworkErrorsTable(networkErrors), + errors_present: hasErrors, + elements: buildElementsArray(issue.elements), + ...buildCustomAttributeTokens(issue.elements), + }; +} + +// ============================================================================ +// Tests +// ============================================================================ + +describe('Custom Templates v1.1.8 - Kitchen Sink', () => { + describe('All Tags Render Correctly', () => { + it('should render all tags with fix issue, React source, and all errors', () => { + const context = buildTestContext(mockIssueWithReactSource, mockConsoleErrors, mockNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + // Critical assertion: no unrendered tokens should remain + expect(result).toHaveNoUnrenderedTokens(); + + // Verify issue metadata tags + expect(result).toContain('issue-react-301'); + expect(result).toContain('React Component Bug'); + expect(result).toContain('fix'); + expect(result).toContain('Fix'); + expect(result).toContain('Bug Fix'); + expect(result).toContain('https://example.com/login'); + expect(result).toContain('2024-02-01'); + + // Verify user prompt + expect(result).toContain('The LoginButton component does not trigger onClick'); + expect(result).toContain('> The LoginButton component does not trigger onClick'); + + // Verify elements + expect(result).toContain('1 element(s)'); + expect(result).toContain('#login-btn'); + + // Verify {{#each elements}} iteration + expect(result).toContain('#### Element 1 of 1'); + expect(result).toContain('**Index:** 0'); + expect(result).toContain('**First:** true'); + expect(result).toContain('**Last:** true'); + + // Verify React source tokens + expect(result).toContain('**React Component:** `LoginButton`'); + expect(result).toContain('**File Path:** src/components/LoginButton.tsx'); + expect(result).toContain('**Line:** 42'); + expect(result).toContain('**Column:** 8'); + expect(result).toContain('**Location:** src/components/LoginButton.tsx:42:8'); + expect(result).toContain('→ App'); + expect(result).toContain('→ LoginButton'); + + // Verify console errors section appears + expect(result).toContain('## Console Errors'); + expect(result).toContain('2 error(s) detected'); + + // Verify network errors section appears + expect(result).toContain('## Network Errors'); + expect(result).toContain('3 failed request(s)'); + + // Verify error summary section appears + expect(result).toContain('## Error Summary'); + + // Verify custom attribute section appears (global) + expect(result).toContain('## Global Custom Attribute: data-testid'); + expect(result).toContain('login-button'); + }); + + it('should render all tags with enhancement issue', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toHaveNoUnrenderedTokens(); + + // Verify enhancement-specific values + expect(result).toContain('Enhancement'); + expect(result).toContain('Modify'); + expect(result).toContain('enhancement'); + expect(result).toContain('Add Dark Mode Toggle'); + }); + + it('should render with multiple elements showing each iteration', () => { + const context = buildTestContext(mockMultiElementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toHaveNoUnrenderedTokens(); + + // Verify multiple elements count + expect(result).toContain('3 element(s)'); + + // Verify each iteration with special variables + expect(result).toContain('#### Element 1 of 3'); + expect(result).toContain('#### Element 2 of 3'); + expect(result).toContain('#### Element 3 of 3'); + + // Verify first/last indicators + // Check that first element has First: true + expect(result).toContain('**First:** true'); + // Check that last element has Last: true + expect(result).toContain('**Last:** true'); + + // Verify all selectors are present + expect(result).toContain('#email'); + expect(result).toContain('#password'); + expect(result).toContain('button.submit-btn'); + }); + + it('should render with mixed React source (some elements have it, some dont)', () => { + const context = buildTestContext(mockIssueWithMixedReactSource, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toHaveNoUnrenderedTokens(); + + // Elements 1 and 3 have React source + expect(result).toContain('EmailInput'); + expect(result).toContain('SubmitButton'); + + // Element 2 does not have React source + // The React Component section should only appear for elements 1 and 3 + }); + + it('should render without custom attributes', () => { + const context = buildTestContext(mockIssueNoCustomAttrs, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toHaveNoUnrenderedTokens(); + + // Global custom attribute sections should not appear + expect(result).not.toContain('## Global Custom Attribute: data-testid'); + expect(result).not.toContain('## Global Custom Attribute: data-qa'); + }); + + it('should render with empty user prompt', () => { + const context = buildTestContext(mockIssueEmptyPrompt, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toHaveNoUnrenderedTokens(); + + // Should use default blockquote text + expect(result).toContain('_No description provided'); + }); + + it('should render with multiple custom attributes', () => { + const context = buildTestContext(mockIssueMultipleCustomAttrs, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toHaveNoUnrenderedTokens(); + + // Both custom attribute sections should appear + expect(result).toContain('## Global Custom Attribute: data-testid'); + expect(result).toContain('main-card'); + expect(result).toContain('## Global Custom Attribute: data-qa'); + expect(result).toContain('card-123'); + }); + + it('should suppress minified React source', () => { + const context = buildTestContext(mockIssueWithMinifiedReact, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toHaveNoUnrenderedTokens(); + + // Should not contain React component info for minified + expect(result).not.toContain('**React Component:** `n`'); + expect(result).not.toContain('→ a'); + expect(result).not.toContain('→ b'); + }); + }); + + describe('Conditional Sections', () => { + it('should hide console errors section when no console errors', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).not.toContain('## Console Errors'); + expect(result).not.toContain('error(s) detected'); + }); + + it('should hide network errors section when no network errors', () => { + const context = buildTestContext(mockFixIssue, mockConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).not.toContain('## Network Errors'); + expect(result).not.toContain('failed request(s)'); + }); + + it('should hide error summary when no errors at all', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).not.toContain('## Error Summary'); + }); + + it('should show error summary when only console errors present', () => { + const context = buildTestContext(mockFixIssue, mockConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toContain('## Error Summary'); + }); + + it('should show error summary when only network errors present', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toContain('## Error Summary'); + }); + + it('should hide custom attribute section when attribute not present', () => { + const context = buildTestContext(mockIssueNoCustomAttrs, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).not.toContain('## Global Custom Attribute: data-testid'); + expect(result).not.toContain('## Global Custom Attribute: data-qa'); + }); + + it('should hide React source section for elements without React', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + // Element doesn't have React source + expect(result).not.toContain('**React Component:**'); + expect(result).not.toContain('**Component Stack:**'); + }); + }); + + describe('Special Variables in Each Blocks', () => { + it('should correctly set @first and @last for multiple elements', () => { + const context = buildTestContext(mockMultiElementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + // The template outputs @first and @last values + // First element: @first=true, @last=empty + // Middle element: @first=empty, @last=empty + // Last element: @first=empty, @last=true + + // We need to check that the pattern appears correctly + expect(result).toContain('**First:** true'); + expect(result).toContain('**Last:** true'); + }); + + it('should correctly set @count for all elements', () => { + const context = buildTestContext(mockMultiElementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + // All elements should show "of 3" in the header + expect(result).toContain('Element 1 of 3'); + expect(result).toContain('Element 2 of 3'); + expect(result).toContain('Element 3 of 3'); + }); + + it('should correctly set @index (zero-based)', () => { + const context = buildTestContext(mockMultiElementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toContain('**Index:** 0'); + expect(result).toContain('**Index:** 1'); + expect(result).toContain('**Index:** 2'); + }); + }); + + describe('Value Correctness', () => { + it('should correctly format issue.type_label for fix', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('Label: {{issue.type_label}}', context); + + expect(result).toBe('Label: Fix'); + }); + + it('should correctly format issue.type_label for enhancement', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('Label: {{issue.type_label}}', context); + + expect(result).toBe('Label: Modify'); + }); + + it('should correctly format issue.type_title for fix', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('Title: {{issue.type_title}}', context); + + expect(result).toBe('Title: Bug Fix'); + }); + + it('should correctly format issue.type_title for enhancement', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('Title: {{issue.type_title}}', context); + + expect(result).toBe('Title: Enhancement'); + }); + + it('should correctly format timestamp as ISO string', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('Time: {{issue.timestamp_iso}}', context); + + expect(result).toBe('Time: 2024-02-01T00:00:00.000Z'); + }); + + it('should correctly count elements', () => { + const context = buildTestContext(mockMultiElementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('Count: {{elements_count}}', context); + + expect(result).toBe('Count: 3'); + }); + + it('should correctly count console errors', () => { + const context = buildTestContext(mockFixIssue, mockConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('Errors: {{console_errors_count}}', context); + + expect(result).toBe('Errors: 2'); + }); + + it('should correctly count network errors', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockNetworkErrors); + const result = renderTemplate('Errors: {{network_errors_count}}', context); + + expect(result).toBe('Errors: 3'); + }); + + it('should correctly format React file location with line and column', () => { + const context = buildTestContext(mockIssueWithReactSource, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('{{#each elements}}{{element.react.file_location}}{{/each}}', context); + + expect(result).toBe('src/components/LoginButton.tsx:42:8'); + }); + + it('should correctly format React component stack', () => { + const context = buildTestContext(mockIssueWithReactSource, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate('{{#each elements}}{{element.react.component_stack}}{{/each}}', context); + + expect(result).toContain('→ App'); + expect(result).toContain('→ Layout'); + expect(result).toContain('→ Header'); + expect(result).toContain('→ LoginForm'); + expect(result).toContain('→ LoginButton'); + }); + }); + + describe('Snapshot Tests', () => { + it('should match snapshot for kitchen sink with React source and all errors', () => { + const context = buildTestContext(mockIssueWithReactSource, mockConsoleErrors, mockNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toMatchSnapshot('kitchen-sink-react-all-errors'); + }); + + it('should match snapshot for kitchen sink with no errors', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toMatchSnapshot('kitchen-sink-no-errors'); + }); + + it('should match snapshot for kitchen sink with multiple elements', () => { + const context = buildTestContext(mockMultiElementIssue, mockConsoleErrors, mockNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toMatchSnapshot('kitchen-sink-multi-elements'); + }); + + it('should match snapshot for kitchen sink with mixed React source', () => { + const context = buildTestContext(mockIssueWithMixedReactSource, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toMatchSnapshot('kitchen-sink-mixed-react'); + }); + + it('should match snapshot for enhancement issue', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toMatchSnapshot('kitchen-sink-enhancement'); + }); + + it('should match snapshot for minified React (suppressed)', () => { + const context = buildTestContext(mockIssueWithMinifiedReact, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(KITCHEN_SINK_TEMPLATE, context); + + expect(result).toMatchSnapshot('kitchen-sink-minified-react'); + }); + }); +}); diff --git a/src/__tests__/templates/v1.1.8/defaultTemplates.test.ts b/src/__tests__/templates/v1.1.8/defaultTemplates.test.ts new file mode 100644 index 0000000..713fc42 --- /dev/null +++ b/src/__tests__/templates/v1.1.8/defaultTemplates.test.ts @@ -0,0 +1,595 @@ +/** + * v1.1.8 - Default Template Tests + * + * Tests the v1.1.8 template format which introduces: + * - {{#each elements}} iteration blocks + * - React source tokens (element.react.*) + * - Minified React detection and suppression + * - Quick select template + * + * These tests use FROZEN template strings to ensure backward compatibility + * is maintained even when DEFAULT_PROMPT_TEMPLATES evolves. + * + * DO NOT MODIFY once a new version folder is created. + */ + +import { renderTemplate, type TemplateContextWithArrays, type TemplateArrayItem } from '@/exporter/PromptTemplateRenderer'; +import { + mockFixIssue, + mockEnhancementIssue, + mockIssueWithReactSource, + mockIssueWithMixedReactSource, + mockIssueWithMinifiedReact, + mockIssueWithReactNameOnly, + mockMultiElementIssue, + mockSingleConsoleError, + mockConsoleErrors, + mockEmptyConsoleErrors, + mockSingleNetworkError, + mockNetworkErrors, + mockEmptyNetworkErrors, + V1_1_8_FIX_TEMPLATE, + V1_1_8_ENHANCEMENT_TEMPLATE, + V1_1_8_QUICK_SELECT_TEMPLATE, + mockQuickSelectSingleElement, + mockQuickSelectMultipleElements, + mockQuickSelectNoReact, +} from './__mocks__/testData'; +import type { Issue, ConsoleError, NetworkError, CapturedElement, ReactSourceInfo } from '@/shared/types'; + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/** + * Detect if React source info appears to be from a minified build. + */ +function isLikelyMinified(reactSource: ReactSourceInfo): boolean { + const shortNameCount = reactSource.componentStack.filter( + (name) => name.length <= 2 + ).length; + const mainNameShort = reactSource.componentName !== null && reactSource.componentName.length <= 2; + const totalShort = shortNameCount + (mainNameShort ? 1 : 0); + return totalShort >= 3; +} + +/** + * Build React source tokens for a single element. + */ +function buildSingleElementReactTokens(element: CapturedElement): Record { + const reactSource = element.reactSource; + + if (!reactSource || isLikelyMinified(reactSource)) { + return { + react_source_present: false, + 'react.component_name': '', + 'react.file_path': '', + 'react.line_number': '', + 'react.column_number': '', + 'react.component_stack': '', + 'react.file_location': '', + }; + } + + let fileLocation = ''; + if (reactSource.filePath) { + fileLocation = reactSource.filePath; + if (reactSource.lineNumber) { + fileLocation += `:${reactSource.lineNumber}`; + if (reactSource.columnNumber) { + fileLocation += `:${reactSource.columnNumber}`; + } + } + } + + const componentStackStr = reactSource.componentStack.length > 0 + ? reactSource.componentStack.map((name) => ` → ${name}`).join('\n') + : ''; + + return { + react_source_present: true, + 'react.component_name': reactSource.componentName || '', + 'react.file_path': reactSource.filePath || '', + 'react.line_number': reactSource.lineNumber?.toString() || '', + 'react.column_number': reactSource.columnNumber?.toString() || '', + 'react.component_stack': componentStackStr, + 'react.file_location': fileLocation, + }; +} + +/** + * Build custom attribute tokens for a single element. + */ +function buildSingleElementCustomAttributes(element: CapturedElement): Record { + const tokens: Record = {}; + if (element.customAttributes) { + for (const attr of element.customAttributes) { + tokens[attr.tokenName] = attr.value; + tokens[`${attr.tokenName}_present`] = true; + } + } + return tokens; +} + +/** + * Build elements array for {{#each elements}} iteration. + */ +function buildElementsArray(elements: CapturedElement[]): TemplateArrayItem[] { + return elements.map((element) => { + const reactTokens = buildSingleElementReactTokens(element); + const customAttrTokens = buildSingleElementCustomAttributes(element); + + return { + html: formatHTML(element.html), + html_raw: element.html, + selector: element.selector, + ...reactTokens, + ...customAttrTokens, + }; + }); +} + +function formatHTML(html: string): string { + let formatted = html.replace(/>\n<').replace(/>\s+\n<'); + if (formatted.length > 5000) { + formatted = formatted.substring(0, 5000) + '\n'; + } + return formatted; +} + +function buildElementsMarkdown(elements: CapturedElement[]): string { + if (elements.length === 0) return ''; + const lines: string[] = []; + if (elements.length === 1) { + lines.push('```html'); + lines.push(formatHTML(elements[0].html)); + lines.push('```'); + lines.push(''); + lines.push(`**CSS Selector:** \`${elements[0].selector}\``); + } else { + elements.forEach((element, index) => { + lines.push(`### Element ${index + 1}`); + lines.push(''); + lines.push('```html'); + lines.push(formatHTML(element.html)); + lines.push('```'); + lines.push(''); + lines.push(`**CSS Selector:** \`${element.selector}\``); + if (index < elements.length - 1) { + lines.push(''); + } + }); + } + return lines.join('\n').trimEnd(); +} + +function truncate(str: string, maxLen: number): string { + if (str.length <= maxLen) return str; + return str.substring(0, maxLen - 3) + '...'; +} + +function buildConsoleErrorsMarkdown(consoleErrors: ConsoleError[]): string { + if (consoleErrors.length === 0) return ''; + const lines: string[] = []; + const errorsToShow = consoleErrors.slice(0, 15); + errorsToShow.forEach((error, index) => { + lines.push('### Error'); + lines.push('```'); + lines.push(truncate(error.message, 500)); + lines.push('```'); + if (error.stackTrace) { + lines.push(''); + lines.push('**Stack trace:**'); + lines.push('```'); + const stackLines = error.stackTrace.split('\n').slice(0, 5); + lines.push(stackLines.join('\n')); + lines.push('```'); + } + if (error.url) { + lines.push(`**Source:** \`${error.url}${error.lineNumber ? `:${error.lineNumber}` : ''}\``); + } + if (index < errorsToShow.length - 1) { + lines.push(''); + } + }); + return lines.join('\n').trimEnd(); +} + +function buildNetworkErrorsTable(networkErrors: NetworkError[]): string { + if (networkErrors.length === 0) return ''; + const errorsToShow = networkErrors.slice(0, 15); + const lines = errorsToShow.map((error) => { + const shortUrl = truncate(error.url, 80); + const status = error.status === 0 ? 'CORS/Network' : error.status.toString(); + return `| ${status} | ${error.method} | \`${shortUrl}\` |`; + }); + return lines.join('\n'); +} + +function buildCustomAttributeTokens(elements: CapturedElement[]): Record { + const customAttrMap = new Map(); + for (const element of elements) { + if (element.customAttributes) { + for (const attr of element.customAttributes) { + if (!customAttrMap.has(attr.tokenName)) { + customAttrMap.set(attr.tokenName, attr.value); + } + } + } + } + const tokens: Record = {}; + for (const [tokenName, value] of customAttrMap) { + tokens[tokenName] = value; + tokens[`${tokenName}_present`] = true; + } + return tokens; +} + +/** + * Build template context from issue and errors. + * This mirrors the logic in MarkdownExporter.buildTemplateContext() + */ +function buildTestContext( + issue: Issue, + consoleErrors: ConsoleError[], + networkErrors: NetworkError[] +): TemplateContextWithArrays { + const isEnhancement = issue.type === 'enhancement'; + const issueName = issue.name || 'Untitled'; + const hasErrors = consoleErrors.length > 0 || networkErrors.length > 0; + + const userPromptBlockquote = issue.userPrompt + ? `> ${issue.userPrompt.split('\n').join('\n> ')}` + : '_No description provided. Examine the selected element and errors for context._'; + + const firstElement = issue.elements[0]; + const elementHtml = firstElement + ? `\`\`\`html\n${formatHTML(firstElement.html)}\n\`\`\`` + : ''; + const elementCssSelector = firstElement + ? `**CSS Selector:** \`${firstElement.selector}\`` + : ''; + + return { + 'issue.id': issue.id, + 'issue.name': issueName, + 'issue.type': issue.type, + 'issue.type_label': isEnhancement ? 'Modify' : 'Fix', + 'issue.type_title': isEnhancement ? 'Enhancement' : 'Bug Fix', + 'issue.page_url': issue.pageUrl, + 'issue.user_prompt': issue.userPrompt || '', + 'issue.user_prompt_blockquote': userPromptBlockquote, + 'issue.timestamp_iso': new Date(issue.timestamp).toISOString(), + elements_count: issue.elements.length, + elements_markdown: buildElementsMarkdown(issue.elements), + elements_html_first: issue.elements[0] ? formatHTML(issue.elements[0].html) : '', + elements_selector_first: issue.elements[0]?.selector || '', + 'element.html': elementHtml, + 'element.css_selector': elementCssSelector, + console_errors_count: consoleErrors.length, + console_errors_present: consoleErrors.length > 0, + console_errors_markdown: buildConsoleErrorsMarkdown(consoleErrors), + network_errors_count: networkErrors.length, + network_errors_present: networkErrors.length > 0, + network_errors_table: buildNetworkErrorsTable(networkErrors), + errors_present: hasErrors, + elements: buildElementsArray(issue.elements), + ...buildCustomAttributeTokens(issue.elements), + }; +} + +/** + * Build template context for quick select mode. + */ +function buildQuickSelectContext(elements: CapturedElement[], pageUrl: string): TemplateContextWithArrays { + return { + page_url: pageUrl, + elements_count: elements.length, + elements_multiple: elements.length > 1, + elements: buildElementsArray(elements), + ...buildCustomAttributeTokens(elements), + }; +} + +// ============================================================================ +// Tests +// ============================================================================ + +describe('Default Templates v1.1.8', () => { + describe('Fix Template with {{#each elements}}', () => { + it('should render with single element and no React source', () => { + const context = buildTestContext(mockFixIssue, mockConsoleErrors, mockNetworkErrors); + const result = renderTemplate(V1_1_8_FIX_TEMPLATE, context); + + // Check main sections are present + expect(result).toContain('# Bug Fix'); + expect(result).toContain('## Your Task'); + expect(result).toContain('## What the User Wants'); + expect(result).toContain('## Context'); + expect(result).toContain('## Target Element(s)'); + expect(result).toContain('## Console Errors'); + expect(result).toContain('## Failed Network Requests'); + expect(result).toContain('## Summary'); + + // Check content + expect(result).toContain('> The login button does not respond when clicked'); + expect(result).toContain('https://example.com/login'); + expect(result).toContain('#login-btn'); + + // Check element is rendered via {{#each}} + expect(result).toContain('### Element 1'); + expect(result).toContain(''); + + expect(result).toHaveNoUnrenderedTokens(); + }); + + it('should render React source with only component name (no file location)', () => { + const context = buildTestContext(mockIssueWithReactNameOnly, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(V1_1_8_ENHANCEMENT_TEMPLATE, context); + + // Component name should be present + expect(result).toContain('**React Component:** `UserProfile`'); + // File location should be empty (but the at `` should still be there) + expect(result).toContain('at ``'); + // Component stack should still be present + expect(result).toContain('→ App'); + expect(result).toContain('→ Dashboard'); + expect(result).toContain('→ UserProfile'); + + expect(result).toHaveNoUnrenderedTokens(); + }); + + it('should hide console errors section when no console errors', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockNetworkErrors); + const result = renderTemplate(V1_1_8_FIX_TEMPLATE, context); + + expect(result).not.toContain('## Console Errors'); + expect(result).toContain('## Failed Network Requests'); + expect(result).toHaveNoUnrenderedTokens(); + }); + + it('should hide network errors section when no network errors', () => { + const context = buildTestContext(mockFixIssue, mockConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(V1_1_8_FIX_TEMPLATE, context); + + expect(result).toContain('## Console Errors'); + expect(result).not.toContain('## Failed Network Requests'); + expect(result).toHaveNoUnrenderedTokens(); + }); + + it('should hide both error sections when no errors', () => { + const context = buildTestContext(mockFixIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(V1_1_8_FIX_TEMPLATE, context); + + expect(result).not.toContain('## Console Errors'); + expect(result).not.toContain('## Failed Network Requests'); + expect(result).toHaveNoUnrenderedTokens(); + }); + }); + + describe('Enhancement Template', () => { + it('should render enhancement template with React source info', () => { + const context = buildTestContext(mockIssueWithReactNameOnly, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(V1_1_8_ENHANCEMENT_TEMPLATE, context); + + // Check enhancement-specific content + expect(result).toContain('# Enhancement'); + expect(result).toContain('add or change functionality'); + + // Check React source + expect(result).toContain('**React Component:** `UserProfile`'); + + expect(result).toHaveNoUnrenderedTokens(); + }); + + it('should render enhancement template without errors', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(V1_1_8_ENHANCEMENT_TEMPLATE, context); + + expect(result).toContain('# Enhancement'); + expect(result).not.toContain('## Console Errors'); + expect(result).not.toContain('## Failed Network Requests'); + expect(result).toHaveNoUnrenderedTokens(); + }); + + it('should include suggested approach for enhancement', () => { + const context = buildTestContext(mockEnhancementIssue, mockEmptyConsoleErrors, mockEmptyNetworkErrors); + const result = renderTemplate(V1_1_8_ENHANCEMENT_TEMPLATE, context); + + expect(result).toContain('**Suggested approach:**'); + expect(result).toContain('Locate the target element in the codebase'); + expect(result).toContain('Implement the requested enhancement'); + }); + }); + + describe('Quick Select Template', () => { + it('should render single element', () => { + const context = buildQuickSelectContext(mockQuickSelectSingleElement, 'https://example.com/form'); + const result = renderTemplate(V1_1_8_QUICK_SELECT_TEMPLATE, context); + + expect(result).toContain('## Element 1'); + expect(result).toContain('