--- title: How to Implement a Cosine Similarity Function in TypeScript for Vector Comparison description: Learn how to build an efficient cosine similarity function in TypeScript for comparing vector embeddings. This step-by-step guide includes code examples, performance optimizations, and practical applications for semantic search and AI recommendation systems tags: ['typescript', 'ai', 'mathematics'] --- # How to Implement a Cosine Similarity Function in TypeScript for Vector Comparison To understand how an AI can understand that the word "cat" is similar to "kitten," you must realize cosine similarity. In short, with the help of embeddings, we can represent words as vectors in a high-dimensional space. If the word "cat" is represented as a vector [1, 0, 0], the word "kitten" would be represented as [1, 0, 1]. Now, we can use cosine similarity to measure the similarity between the two vectors. In this blog post, we will break down the concept of cosine similarity and implement it in TypeScript. I won't explain how embeddings work in this blog post, but only how to use them. ## Why Cosine Similarity Matters for Modern Web Development When you build applications with any of these features, you directly work with vector mathematics: - **Semantic search**: Finding relevant content based on meaning, not just keywords - **AI-powered recommendations**: "Users who liked this also enjoyed..." - **Content matching**: Identifying similar articles, products, or user profiles - **Natural language processing**: Understanding and comparing text meaning All of these require you to compare vectors, and cosine similarity offers one of the most effective methods to do so. Let me break down this concept from the ground up. ## First, Let's Understand Vectors Vectors form the foundation of many AI operations. But what exactly makes a vector? We express a vector $\vec{v}$ in $n$-dimensional space as: $$\vec{v} = [v_1, v_2, ..., v_n]$$ Each $v_i$ stands for a component or coordinate in the $i$-th dimension. While we've only examined 2D vectors here, modern embeddings contain hundreds of dimensions. ## What Is Cosine Similarity and How Does It Work? Now that you understand vectors, let's examine how to measure similarity between them using cosine similarity: ### Cosine Similarity Explained Cosine similarity measures the cosine of the angle between two vectors, showing how similar they are regardless of their magnitude. The value ranges from: - **+1**: When vectors point in the same direction (perfectly similar) - **0**: When vectors stand perpendicular (no similarity) - **-1**: When vectors point in opposite directions (completely dissimilar) With the interactive visualization above, you can: 1. Move both vectors by dragging the colored circles at their endpoints 2. Observe how the angle between them changes 3. See how cosine similarity relates to this angle 4. Note that cosine similarity depends only on the angle, not the vectors' lengths #### Simple Explanation in Plain English The cosine similarity formula measures how similar two vectors are by examining the angle between them, not their sizes. Here's how it works in plain English: 1. **What it does**: It tells you if two vectors point in the same direction, opposite directions, or somewhere in between. 2. **The calculation**: - First, multiply the corresponding elements of both vectors and add these products together (the dot product) - Then, calculate how long each vector is (its magnitude) - Finally, divide the dot product by the product of the two magnitudes 3. **The result**: - If you get 1, the vectors point in exactly the same direction (perfectly similar) - If you get 0, the vectors stand perpendicular to each other (completely unrelated) - If you get -1, the vectors point in exactly opposite directions (perfectly dissimilar) - Any value in between indicates the degree of similarity 4. **Why it's useful**: - It ignores vector size and focuses only on direction - This means you can consider two things similar even if one is much "bigger" than the other - For example, a short document about cats and a long document about cats would show similarity, despite their different lengths 5. **In AI applications**: - We convert words, documents, images, etc. into vectors with many dimensions - Cosine similarity helps us find related items by measuring how closely their vectors align - This powers features like semantic search, recommendations, and content matching ## Step-by-Step Example Calculation Let me walk you through a manual calculation of cosine similarity between two simple vectors. This helps build intuition before we implement it in code. Given two vectors: $\vec{v_1} = [3, 4]$ and $\vec{v_2} = [5, 2]$ I'll calculate their cosine similarity step by step: **Step 1**: Calculate the dot product. $$ \vec{v_1} \cdot \vec{v_2} = 3 \times 5 + 4 \times 2 = 15 + 8 = 23 $$ **Step 2**: Calculate the magnitude of each vector. $$ ||\vec{v_1}|| = \sqrt{3^2 + 4^2} = \sqrt{9 + 16} = \sqrt{25} = 5 $$ $$ ||\vec{v_2}|| = \sqrt{5^2 + 2^2} = \sqrt{25 + 4} = \sqrt{29} \approx 5.385 $$ **Step 3**: Calculate the cosine similarity by dividing the dot product by the product of magnitudes. $$ \cos(\theta) = \frac{\vec{v_1} \cdot \vec{v_2}}{||\vec{v_1}|| \cdot ||\vec{v_2}||} $$ $$ = \frac{23}{5 \times 5.385} = \frac{23}{26.925} \approx 0.854 $$ Therefore, the cosine similarity between vectors $$\vec{v_1}$$ and $$ \vec{v_2} $$ is approximately 0.854, which shows that these vectors point in roughly the same direction. ### Why Use Cosine Similarity? Cosine similarity offers particular advantages in AI applications because: 1. **It ignores magnitude**: You can find similarity between two documents even if one contains many more words than the other 2. **It handles high dimensions efficiently**: It scales effectively to the hundreds or thousands of dimensions used in AI embeddings 3. **It captures semantic meaning**: The angle between vectors often correlates well with conceptual similarity ## Building a Cosine Similarity Function in TypeScript Let me implement a cosine similarity function in TypeScript. This gives you complete control and understanding of the process. ```typescript /** * Calculates the cosine similarity between two vectors * @param vecA First vector * @param vecB Second vector * @returns A value between -1 and 1, where 1 means identical */ function calculateCosineSimilarity(vecA: number[], vecB: number[]): number { // Verify vectors are of the same length if (vecA.length !== vecB.length) { throw new Error("Vectors must have the same dimensions"); } // Calculate dot product let dotProduct = 0; for (let i = 0; i < vecA.length; i++) { dotProduct += vecA[i] * vecB[i]; } // Calculate magnitudes let magnitudeA = 0; let magnitudeB = 0; for (let i = 0; i < vecA.length; i++) { magnitudeA += vecA[i] * vecA[i]; magnitudeB += vecB[i] * vecB[i]; } magnitudeA = Math.sqrt(magnitudeA); magnitudeB = Math.sqrt(magnitudeB); // Handle zero magnitude if (magnitudeA === 0 || magnitudeB === 0) { return 0; // or throw an error, depending on your requirements } // Calculate and return cosine similarity return dotProduct / (magnitudeA * magnitudeB); } ``` ## A More Efficient Implementation I can improve our implementation using array methods for a more concise, functional approach: ```typescript function cosineSimilarity(vecA: number[], vecB: number[]): number { if (vecA.length !== vecB.length) { throw new Error("Vectors must have the same dimensions"); } // Calculate dot product: A·B = Σ(A[i] * B[i]) const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); // Calculate magnitude of vector A: |A| = √(Σ(A[i]²)) const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0)); // Calculate magnitude of vector B: |B| = √(Σ(B[i]²)) const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0)); // Check for zero magnitude if (magnitudeA === 0 || magnitudeB === 0) { return 0; } // Calculate cosine similarity: (A·B) / (|A|*|B|) return dotProduct / (magnitudeA * magnitudeB); } ``` ### Using Math.hypot() for Performance Optimization You can optimize vector magnitude calculations using the built-in `Math.hypot()` function, which calculates the square root of the sum of squares more efficiently: ```typescript function cosineSimilarityOptimized(vecA: number[], vecB: number[]): number { if (vecA.length !== vecB.length) { throw new Error("Vectors must have the same dimensions"); } // Calculate dot product: A·B = Σ(A[i] * B[i]) const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); // Calculate magnitudes using Math.hypot() const magnitudeA = Math.hypot(...vecA); const magnitudeB = Math.hypot(...vecB); // Check for zero magnitude if (magnitudeA === 0 || magnitudeB === 0) { return 0; } // Calculate cosine similarity: (A·B) / (|A|*|B|) return dotProduct / (magnitudeA * magnitudeB); } ``` This approach is not only more concise but can be significantly faster, especially for larger vectors, as `Math.hypot()` is highly optimized by modern JavaScript engines. ## Testing Our Implementation Let's see how our function works with some example vectors: ```typescript // Example 1: Similar vectors pointing in roughly the same direction const vecA = [3, 4]; const vecB = [5, 2]; console.log(`Similarity: ${cosineSimilarity(vecA, vecB).toFixed(3)}`); // Output: Similarity: 0.857 // Example 2: Perpendicular vectors const vecC = [1, 0]; const vecD = [0, 1]; console.log(`Similarity: ${cosineSimilarity(vecC, vecD).toFixed(3)}`); // Output: Similarity: 0.000 // Example 3: Opposite vectors const vecE = [2, 3]; const vecF = [-2, -3]; console.log(`Similarity: ${cosineSimilarity(vecE, vecF).toFixed(3)}`); // Output: Similarity: -1.000 ``` Mathematically, we can verify these results: For Example 1: $$\text{cosine similarity} = \frac{3 \times 5 + 4 \times 2}{\sqrt{3^2 + 4^2} \times \sqrt{5^2 + 2^2}} = \frac{15 + 8}{\sqrt{25} \times \sqrt{29}} = \frac{23}{5 \times \sqrt{29}} \approx 0.857$$ For Example 2: $$\text{cosine similarity} = \frac{1 \times 0 + 0 \times 1}{\sqrt{1^2 + 0^2} \times \sqrt{0^2 + 1^2}} = \frac{0}{1 \times 1} = 0$$ For Example 3: $$\text{cosine similarity} = \frac{2 \times (-2) + 3 \times (-3)}{\sqrt{2^2 + 3^2} \times \sqrt{(-2)^2 + (-3)^2}} = \frac{-4 - 9}{\sqrt{13} \times \sqrt{13}} = \frac{-13}{13} = -1$$ ## Complete TypeScript Solution Here's a complete TypeScript solution that includes our cosine similarity function along with some utility methods: ```typescript class VectorUtils { /** * Calculates the cosine similarity between two vectors */ static cosineSimilarity(vecA: number[], vecB: number[]): number { if (vecA.length !== vecB.length) { throw new Error(`Vector dimensions don't match: ${vecA.length} vs ${vecB.length}`); } const dotProduct = vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); const magnitudeA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0)); const magnitudeB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0)); if (magnitudeA === 0 || magnitudeB === 0) { return 0; } return dotProduct / (magnitudeA * magnitudeB); } /** * Calculates the dot product of two vectors */ static dotProduct(vecA: number[], vecB: number[]): number { if (vecA.length !== vecB.length) { throw new Error(`Vector dimensions don't match: ${vecA.length} vs ${vecB.length}`); } return vecA.reduce((sum, a, i) => sum + a * vecB[i], 0); } /** * Calculates the magnitude (length) of a vector */ static magnitude(vec: number[]): number { return Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0)); } /** * Normalizes a vector (converts to unit vector) */ static normalize(vec: number[]): number[] { const mag = this.magnitude(vec); if (mag === 0) { return Array(vec.length).fill(0); } return vec.map(v => v / mag); } /** * Converts cosine similarity to angular distance in degrees */ static similarityToDegrees(similarity: number): number { // Clamp similarity to [-1, 1] to handle floating point errors const clampedSimilarity = Math.max(-1, Math.min(1, similarity)); return Math.acos(clampedSimilarity) * (180 / Math.PI); } } ``` The angular distance formula uses the inverse cosine function: $$ \theta = \cos^{-1}(\text{cosine similarity}) \times \frac{180}{\pi} $$ Where $\theta$ represents the angle in degrees between the two vectors. ## Using Cosine Similarity in Real Web Applications When you work with AI in web applications, you'll often need to calculate similarity between vectors. For example: 1. **Finding similar products**: ```typescript function findSimilarProducts(product: Product, allProducts: Product[]): Product[] { return allProducts .filter(p => p.id !== product.id) // Exclude the current product .map(p => ({ product: p, similarity: VectorUtils.cosineSimilarity(product.embedding, p.embedding) })) .sort((a, b) => b.similarity - a.similarity) // Sort by similarity (highest first) .slice(0, 5) // Get top 5 similar products .map(result => result.product); } ``` 2. **Semantic search**: ```typescript function semanticSearch(queryEmbedding: number[], documentEmbeddings: DocumentWithEmbedding[]): SearchResult[] { return documentEmbeddings .map(doc => ({ document: doc, relevance: VectorUtils.cosineSimilarity(queryEmbedding, doc.embedding) })) .filter(result => result.relevance > 0.7) // Only consider relevant results .sort((a, b) => b.relevance - a.relevance); } ``` ## Using OpenAI Embedding Models with Cosine Similarity While the examples above used simple vectors for clarity, real-world AI applications typically use embedding models that transform text and other data into high-dimensional vector spaces. OpenAI provides powerful embedding models that you can easily incorporate into your applications. These models transform text into vectors with hundreds or thousands of dimensions that capture semantic meaning: ```typescript // Example of using OpenAI embeddings with our cosine similarity function async function compareTextSimilarity(textA: string, textB: string): Promise { // Get embeddings from OpenAI API const responseA = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'text-embedding-3-large', input: textA }) }); const responseB = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'text-embedding-3-large', input: textB }) }); const embeddingA = (await responseA.json()).data[0].embedding; const embeddingB = (await responseB.json()).data[0].embedding; // Calculate similarity using our function return VectorUtils.cosineSimilarity(embeddingA, embeddingB); } ``` In a production environment, you should pre-compute embeddings for your content (like blog posts, products, or documents) and store them in a vector database (like Pinecone, Qdrant, or Milvus). Re-computing embeddings for every user request as shown in this example wastes resources and slows performance. A better approach: embed your content once during indexing, store the vectors, and only embed the user's query when performing a search. OpenAI's latest embedding models like `text-embedding-3-large` have up to 3,072 dimensions, capturing extremely nuanced semantic relationships between words and concepts. These high-dimensional embeddings enable much more accurate similarity measurements than simpler vector representations. For more information on OpenAI's embedding models, including best practices and implementation details, check out their documentation at [https://platform.openai.com/docs/guides/embeddings](https://platform.openai.com/docs/guides/embeddings). ## Conclusion Understanding vectors and cosine similarity provides practical tools that empower you to work effectively with modern AI features. By implementing these concepts in TypeScript, you gain a deeper understanding and precise control over calculating similarity in your applications. The interactive visualizations we've explored help you build intuition about these mathematical concepts, while the TypeScript implementation gives you the tools to apply them in real-world scenarios. Whether you build recommendation systems, semantic search, or content-matching features, the foundation you've gained here will help you implement more intelligent, accurate, and effective AI-powered features in your web applications. --- --- title: How I Added llms.txt to My Astro Blog description: I built a simple way to load my blog content into any LLM with one click. This post shows how you can do it too. tags: ['astro', 'ai'] --- # How I Added llms.txt to My Astro Blog ## TLDR I created an endpoint in my Astro blog that outputs all posts in plain text format. This lets me copy my entire blog with one click and paste it into any LLM with adequate context window. The setup uses TypeScript and Astro's API routes, making it work with any Astro content collection. ## Why I Built This I wanted a quick way to ask AI models questions about my own blog content. Copying posts one by one is slow. With this solution, I can give any LLM all my blog posts at once. ## How It Works The solution creates a special endpoint that: 1. Gets all blog posts 2. Converts them to plain text 3. Formats them with basic metadata 4. Outputs everything as one big text file ## Setting Up the File First, I created a new TypeScript file in my Astro pages directory: ```ts // src/pages/llms.txt.ts // Function to extract the frontmatter as text const extractFrontmatter = (content: string): string => { const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); return frontmatterMatch ? frontmatterMatch[1] : ''; }; // Function to clean content while keeping frontmatter const cleanContent = (content: string): string => { // Extract the frontmatter as text const frontmatterText = extractFrontmatter(content); // Remove the frontmatter delimiters let cleanedContent = content.replace(/^---\n[\s\S]*?\n---/, ''); // Clean up MDX-specific imports cleanedContent = cleanedContent.replace(/import\s+.*\s+from\s+['"].*['"];?\s*/g, ''); // Remove MDX component declarations cleanedContent = cleanedContent.replace(/<\w+\s+.*?\/>/g, ''); // Remove Shiki Twoslash syntax like cleanedContent = cleanedContent.replace(/\/\/\s*@noErrors/g, ''); cleanedContent = cleanedContent.replace(/\/\/\s*@(.*?)$/gm, ''); // Remove other Shiki Twoslash directives // Clean up multiple newlines cleanedContent = cleanedContent.replace(/\n\s*\n\s*\n/g, '\n\n'); // Return the frontmatter as text, followed by the cleaned content return frontmatterText + '\n\n' + cleanedContent.trim(); }; export const GET: APIRoute = async () => { try { // Get all blog posts sorted by date (newest first) const posts = await getCollection('blog', ({ data }) => !data.draft); const sortedPosts = posts.sort((a, b) => new Date(b.data.pubDatetime).valueOf() - new Date(a.data.pubDatetime).valueOf() ); // Generate the content let llmsContent = ''; for (const post of sortedPosts) { // Add post metadata in the format similar to the example llmsContent += `--- title: ${post.data.title} description: ${post.data.description} tags: [${post.data.tags.map(tag => `'${tag}'`).join(', ')}] ---\n\n`; // Add the post title as a heading llmsContent += `# ${post.data.title}\n\n`; // Process the content, keeping frontmatter as text const processedContent = cleanContent(post.body); llmsContent += processedContent + '\n\n'; // Add separator between posts llmsContent += '---\n\n'; } // Return the response as plain text return new Response(llmsContent, { headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } catch (error) { console.error('Failed to generate llms.txt:', error); return new Response('Error generating llms.txt', { status: 500 }); } }; ``` This code accomplishes four key functions: 1. It uses Astro's `getCollection` function to grab all published blog posts 2. It sorts them by date with newest first 3. It cleans up each post's content with helper functions 4. It formats each post with its metadata and content 5. It returns everything as plain text ## How to Use It Using this is simple: 1. Visit `alexop.dev/llms.txt` in your browser 2. Press Ctrl+A (or Cmd+A on Mac) to select all the text 3. Copy it (Ctrl+C or Cmd+C) 4. Paste it into any LLM with adequate context window (like ChatGPT, Claude, Llama, etc.) 5. Ask questions about your blog content The LLM now has all your blog posts in its context window. You can ask questions such as: - "What topics have I written about?" - "Summarize my post about [topic]" - "Find code examples in my posts that use [technology]" - "What have I written about [specific topic]?" ## Benefits of This Approach This approach offers distinct advantages: - Works with any Astro blog - Requires a single file to set up - Makes your content easy to query with any LLM - Keeps useful metadata with each post - Formats content in a way LLMs understand well ## Conclusion By adding one straightforward TypeScript file to your Astro blog, you can create a fast way to chat with your own content using any LLM with adequate context window. This makes it easy to: - Find information in your old posts - Get summaries of your content - Find patterns across your writing - Generate new ideas based on your past content Give it a try! The setup takes minutes, and it makes interacting with your blog content much faster. --- --- title: How to Do Visual Regression Testing in Vue with Vitest? description: Learn how to implement visual regression testing in Vue.js using Vitest's browser mode. This comprehensive guide covers setting up screenshot-based testing, creating component stories, and integrating with CI/CD pipelines for automated visual testing. tags: ['vue', 'testing', 'vitest'] --- # How to Do Visual Regression Testing in Vue with Vitest? TL;DR: Visual regression testing detects unintended UI changes by comparing screenshots. With Vitest's experimental browser mode and Playwright, you can: - **Run tests in a real browser environment** - **Define component stories for different states** - **Capture screenshots and compare them with baseline images using snapshot testing** In this guide, you'll learn how to set up visual regression testing for Vue components using Vitest. Our test will generate this screenshot: Visual regression testing captures screenshots of UI components and compares them against baseline images to flag visual discrepancies. This ensures consistent styling and layout across your design system. ## Vitest Configuration Start by configuring Vitest with the Vue plugin: ```typescript export default defineConfig({ plugins: [vue()], }) ``` ## Setting Up Browser Testing Visual regression tests need a real browser environment. Install these dependencies: ```bash npm install -D vitest @vitest/browser playwright ``` You can also use the following command to initialize the browser mode: ```bash npx vitest init browser ``` First, configure Vitest to support both unit and browser tests using a workspace file, `vitest.workspace.ts`. For more details on workspace configuration, see the [Vitest Workspace Documentation](https://vitest.dev/guide/workspace.html). Using a workspace configuration allows you to maintain separate settings for unit and browser tests while sharing common configuration. This makes it easier to manage different testing environments in your project. ```typescript export default defineWorkspace([ { extends: './vitest.config.ts', test: { name: 'unit', include: ['**/*.spec.ts', '**/*.spec.tsx'], exclude: ['**/*.browser.spec.ts', '**/*.browser.spec.tsx'], environment: 'jsdom', }, }, { extends: './vitest.config.ts', test: { name: 'browser', include: ['**/*.browser.spec.ts', '**/*.browser.spec.tsx'], browser: { enabled: true, provider: 'playwright', headless: true, instances: [{ browser: 'chromium' }], }, }, }, ]) ``` Add scripts in your `package.json` ```json { "scripts": { "test": "vitest", "test:unit": "vitest --project unit", "test:browser": "vitest --project browser" } } ``` Now we can run tests in separate environments like this: ```bash npm run test:unit npm run test:browser ``` ## The BaseButton Component Consider the `BaseButton.vue` component a reusable button with customizable size, variant, and disabled state: ```vue ``` ## Defining Stories for Testing Create "stories" to showcase different button configurations: ```typescript const buttonStories = [ { name: 'Primary Medium', props: { variant: 'primary', size: 'medium' }, slots: { default: 'Primary Button' }, }, { name: 'Secondary Medium', props: { variant: 'secondary', size: 'medium' }, slots: { default: 'Secondary Button' }, }, // and much more ... ] ``` Each story defines a name, props, and slot content. ## Rendering Stories for Screenshots Render all stories in one container to capture a comprehensive screenshot: ```typescript interface Story { name: string props: Record slots: Record } function renderStories(component: Component, stories: Story[]): HTMLElement { const container = document.createElement('div') container.style.display = 'flex' container.style.flexDirection = 'column' container.style.gap = '16px' container.style.padding = '20px' container.style.backgroundColor = '#ffffff' stories.forEach((story) => { const storyWrapper = document.createElement('div') const label = document.createElement('h3') label.textContent = story.name storyWrapper.appendChild(label) const { container: storyContainer } = render(component, { props: story.props, slots: story.slots, }) storyWrapper.appendChild(storyContainer) container.appendChild(storyWrapper) }) return container } ``` ## Writing the Visual Regression Test Write a test that renders the stories and captures a screenshot: ```typescript // [buttonStories and renderStories defined above] describe('BaseButton', () => { describe('visual regression', () => { it('should match all button variants snapshot', async () => { const container = renderStories(BaseButton, buttonStories) document.body.appendChild(container) const screenshot = await page.screenshot({ path: 'all-button-variants.png', }) // this assertion is acutaly not doing anything // but otherwise you would get a warning about the screenshot not being taken expect(screenshot).toBeTruthy() document.body.removeChild(container) }) }) }) ``` Use `render` from `vitest-browser-vue` to capture components as they appear in a real browser. Save this file with a `.browser.spec.ts` extension (e.g., `BaseButton.browser.spec.ts`) to match your browser test configuration. ## Beyond Screenshots: Automated Comparison Automate image comparison by encoding screenshots in base64 and comparing them against baseline snapshots: ```typescript // Helper function to take and compare screenshots async function takeAndCompareScreenshot(name: string, element: HTMLElement) { const screenshotDir = './__screenshots__' const snapshotDir = './__snapshots__' const screenshotPath = `${screenshotDir}/${name}.png` // Append element to body document.body.appendChild(element) // Take screenshot const screenshot = await page.screenshot({ path: screenshotPath, base64: true, }) // Compare base64 snapshot await expect(screenshot.base64).toMatchFileSnapshot(`${snapshotDir}/${name}.snap`) // Save PNG for reference await expect(screenshot.path).toBeTruthy() // Cleanup document.body.removeChild(element) } ``` Then update the test: ```typescript describe('BaseButton', () => { describe('visual regression', () => { it('should match all button variants snapshot', async () => { const container = renderStories(BaseButton, buttonStories) await expect( takeAndCompareScreenshot('all-button-variants', container) ).resolves.not.toThrow() }) }) }) ``` Vitest is discussing native screenshot comparisons in browser mode. Follow and contribute at [github.com/vitest-dev/vitest/discussions/690](https://github.com/vitest-dev/vitest/discussions/690). ```mermaid flowchart LR A[Render Component] --> B[Capture Screenshot] B --> C{Compare with Baseline} C -->|Match| D[Test Passes] C -->|Difference| E[Review Changes] E -->|Accept| F[Update Baseline] E -->|Reject| G[Fix Component] G --> A ``` ## Conclusion Vitest's experimental browser mode empowers developers to perform accurate visual regression testing of Vue components in real browser environments. While the current workflow requires manual review of screenshot comparisons, it establishes a foundation for more automated visual testing in the future. This approach also strengthens collaboration between developers and UI designers. Designers can review visual changes to components before production deployment by accessing the generated screenshots in the component library. For advanced visual testing capabilities, teams should explore dedicated tools like Playwright or Cypress that offer more features and maturity. Keep in mind to perform visual regression tests against your Base components. --- --- title: How to Test Vue Router Components with Testing Library and Vitest description: Learn how to test Vue Router components using Testing Library and Vitest. This guide covers real router integration, mocked router setups, and best practices for testing navigation, route guards, and dynamic components in Vue applications. tags: ['vue', 'testing', 'vue-router', 'vitest', 'testing-library'] --- # How to Test Vue Router Components with Testing Library and Vitest ## TLDR This guide shows you how to test Vue Router components using real router integration and isolated component testing with mocks. You'll learn to verify router-link interactions, programmatic navigation, and navigation guard handling. ## Introduction Modern Vue applications need thorough testing to ensure reliable navigation and component performance. We'll cover testing strategies using Testing Library and Vitest to simulate real-world scenarios through router integration and component isolation. ## Vue Router Testing Techniques with Testing Library and Vitest Let's explore how to write effective tests for Vue Router components using both real router instances and mocks. ## Testing Vue Router Navigation Components ### Navigation Component Example ```vue ``` ### Real Router Integration Testing Test complete routing behavior with a real router instance: ```typescript describe('NavigationMenu', () => { it('should navigate using router links', async () => { const router = createRouter({ history: createWebHistory(), routes: [ { path: '/dashboard', component: { template: 'Dashboard' } }, { path: '/settings', component: { template: 'Settings' } }, { path: '/profile', component: { template: 'Profile' } }, { path: '/', component: { template: 'Home' } }, ], }) render(NavigationMenu, { global: { plugins: [router], }, }) const user = userEvent.setup() expect(router.currentRoute.value.path).toBe('/') await router.isReady() await user.click(screen.getByText('Dashboard')) expect(router.currentRoute.value.path).toBe('/dashboard') await user.click(screen.getByText('Profile')) expect(router.currentRoute.value.path).toBe('/profile') }) }) ``` ### Mocked Router Testing Test components in isolation with router mocks: ```typescript const mockPush = vi.fn() vi.mock('vue-router', () => ({ useRouter: vi.fn(), })) describe('NavigationMenu with mocked router', () => { it('should handle navigation with mocked router', async () => { const mockRouter = { push: mockPush, currentRoute: { value: { path: '/' } }, } as unknown as Router vi.mocked(useRouter).mockImplementation(() => mockRouter) const user = userEvent.setup() render(NavigationMenu) await user.click(screen.getByText('Profile')) expect(mockPush).toHaveBeenCalledWith('/profile') }) }) ``` ### RouterLink Stub for Isolated Testing Create a RouterLink stub to test navigation without router-link behavior: ```ts // test-utils.ts export const RouterLinkStub: Component = { name: 'RouterLinkStub', props: { to: { type: [String, Object], required: true, }, tag: { type: String, default: 'a', }, exact: Boolean, exactPath: Boolean, append: Boolean, replace: Boolean, activeClass: String, exactActiveClass: String, exactPathActiveClass: String, event: { type: [String, Array], default: 'click', }, }, setup(props) { const router = useRouter() const navigate = () => { router.push(props.to) } return { navigate } }, render() { return h( this.tag, { onClick: () => this.navigate(), }, this.$slots.default?.(), ) }, } ``` Use the RouterLinkStub in tests: ```ts const mockPush = vi.fn() vi.mock('vue-router', () => ({ useRouter: vi.fn(), })) describe('NavigationMenu with mocked router', () => { it('should handle navigation with mocked router', async () => { const mockRouter = { push: mockPush, currentRoute: { value: { path: '/' } }, } as unknown as Router vi.mocked(useRouter).mockImplementation(() => mockRouter) const user = userEvent.setup() render(NavigationMenu, { global: { stubs: { RouterLink: RouterLinkStub, }, }, }) await user.click(screen.getByText('Dashboard')) expect(mockPush).toHaveBeenCalledWith('/dashboard') }) }) ``` ### Testing Navigation Guards Test navigation guards by rendering the component within a route context: ```vue ``` Test the navigation guard: ```ts const routes = [ { path: '/', component: RouteLeaveGuardDemo }, { path: '/about', component: { template: '
About
' } }, ] const router = createRouter({ history: createWebHistory(), routes, }) const App = { template: '' } describe('RouteLeaveGuardDemo', () => { beforeEach(async () => { vi.clearAllMocks() window.confirm = vi.fn() await router.push('/') await router.isReady() }) it('should prompt when guard is triggered and user confirms', async () => { // Set window.confirm to simulate a user confirming the prompt window.confirm = vi.fn(() => true) // Render the component within a router context render(App, { global: { plugins: [router], }, }) const user = userEvent.setup() // Find the 'About' link and simulate a user click const aboutLink = screen.getByRole('link', { name: /About/i }) await user.click(aboutLink) // Assert that the confirm dialog was shown with the correct message expect(window.confirm).toHaveBeenCalledWith('Do you really want to leave this page?') // Verify that the navigation was allowed and the route changed to '/about' expect(router.currentRoute.value.path).toBe('/about') }) }) ``` ### Reusable Router Test Helper Create a helper function to simplify router setup: ```typescript // test-utils.ts // path of the definition of your routes interface RenderWithRouterOptions extends Omit, 'global'> { initialRoute?: string routerOptions?: { routes?: typeof routes history?: ReturnType } } export function renderWithRouter(Component: any, options: RenderWithRouterOptions = {}) { const { initialRoute = '/', routerOptions = {}, ...renderOptions } = options const router = createRouter({ history: createWebHistory(), // Use provided routes or import from your router file routes: routerOptions.routes || routes, }) router.push(initialRoute) return { // Return everything from regular render, plus the router instance ...render(Component, { global: { plugins: [router], }, ...renderOptions, }), router, } } ``` Use the helper in tests: ```typescript describe('NavigationMenu', () => { it('should navigate using router links', async () => { const { router } = renderWithRouter(NavigationMenu, { initialRoute: '/', }) await router.isReady() const user = userEvent.setup() await user.click(screen.getByText('Dashboard')) expect(router.currentRoute.value.path).toBe('/dashboard') }) }) ``` ### Conclusion: Best Practices for Vue Router Component Testing When we test components that rely on the router, we need to consider whether we want to test the functionality in the most realistic use case or in isolation. In my humble opinion, the more you mock a test, the worse it will get. My personal advice would be to aim to use the real router instead of mocking it. Sometimes, there are exceptions, so keep that in mind. Also, you can help yourself by focusing on components that don't rely on router functionality. Reserve router logic for view/page components. While keeping our components simple, we will never have the problem of mocking the router in the first place. --- --- title: How to Use AI for Effective Diagram Creation: A Guide to ChatGPT and Mermaid description: Learn how to leverage ChatGPT and Mermaid to create effective diagrams for technical documentation and communication. tags: ['AI', 'Productivity'] --- # How to Use AI for Effective Diagram Creation: A Guide to ChatGPT and Mermaid ## TLDR Learn how to combine ChatGPT and Mermaid to quickly create professional diagrams for technical documentation. This approach eliminates the complexity of traditional diagramming tools while maintaining high-quality output. ## Introduction Mermaid is a markdown-like script language that generates diagrams from text descriptions. When combined with ChatGPT, it becomes a powerful tool for creating technical diagrams quickly and efficiently. ## Key Diagram Types ### Flowcharts Perfect for visualizing processes: ```plaintext flowchart LR A[Customer selects products] --> B[Customer reviews order] B --> C{Payment Successful?} C -->|Yes| D[Generate Invoice] D --> E[Dispatch goods] C -->|No| F[Redirect to Payment] ``` ```mermaid flowchart LR A[Customer selects products] --> B[Customer reviews order] B --> C{Payment Successful?} C -->|Yes| D[Generate Invoice] D --> E[Dispatch goods] C -->|No| F[Redirect to Payment] ``` ### Sequence Diagrams Ideal for system interactions: ```plaintext sequenceDiagram participant Client participant Server Client->>Server: Request (GET /resource) Server-->>Client: Response (200 OK) ``` ```mermaid sequenceDiagram participant Client participant Server Client->>Server: Request (GET /resource) Server-->>Client: Response (200 OK) ``` ## Using ChatGPT with Mermaid 1. Ask ChatGPT to explain your concept 2. Request a Mermaid diagram representation 3. Iterate on the diagram with follow-up questions Example prompt: "Create a Mermaid sequence diagram showing how Nuxt.js performs server-side rendering" ```plaintext sequenceDiagram participant Client as Client Browser participant Nuxt as Nuxt.js Server participant Vue as Vue.js Application participant API as Backend API Client->>Nuxt: Initial Request Nuxt->>Vue: SSR Starts Vue->>API: API Calls (if any) API-->>Vue: API Responses Vue->>Nuxt: Rendered HTML Nuxt-->>Client: HTML Content ``` ```mermaid sequenceDiagram participant Client as Client Browser participant Nuxt as Nuxt.js Server participant Vue as Vue.js Application participant API as Backend API Client->>Nuxt: Initial Request Nuxt->>Vue: SSR Starts Vue->>API: API Calls (if any) API-->>Vue: API Responses Vue->>Nuxt: Rendered HTML Nuxt-->>Client: HTML Content ``` ## Quick Setup Guide ### Online Editor Use [Mermaid Live Editor](https://mermaid.live/) for quick prototyping. ### VS Code Integration 1. Install "Markdown Preview Mermaid Support" extension 2. Create `.md` file with Mermaid code blocks 3. Preview with built-in markdown viewer ### Web Integration ```html
graph TD A-->B
``` ## Conclusion The combination of ChatGPT and Mermaid streamlines technical diagramming, making it accessible and efficient. Try it in your next documentation project to save time while creating professional diagrams. --- --- title: Building a Pinia Plugin for Cross-Tab State Syncing description: Learn how to create a Pinia plugin that synchronizes state across browser tabs using the BroadcastChannel API and Vue 3's Script Setup syntax. tags: ['Vue', 'Pinia'] --- # Building a Pinia Plugin for Cross-Tab State Syncing ## TLDR Create a Pinia plugin that enables state synchronization across browser tabs using the BroadcastChannel API. The plugin allows you to mark specific stores for cross-tab syncing and handles state updates automatically with timestamp-based conflict resolution. ## Introduction In modern web applications, users often work with multiple browser tabs open. When using Pinia for state management, we sometimes need to ensure that state changes in one tab are reflected across all open instances of our application. This post will guide you through creating a plugin that adds cross-tab state synchronization to your Pinia stores. ## Understanding Pinia Plugins A Pinia plugin is a function that extends the functionality of Pinia stores. Plugins are powerful tools that help: - Reduce code duplication - Add reusable functionality across stores - Keep store definitions clean and focused - Implement cross-cutting concerns ## Cross-Tab Communication with BroadcastChannel The BroadcastChannel API provides a simple way to send messages between different browser contexts (tabs, windows, or iframes) of the same origin. It's perfect for our use case of synchronizing state across tabs. Key features of BroadcastChannel: - Built-in browser API - Same-origin security model - Simple pub/sub messaging pattern - No need for external dependencies ### How BroadcastChannel Works The BroadcastChannel API operates on a simple principle: any browsing context (window, tab, iframe, or worker) can join a channel by creating a `BroadcastChannel` object with the same channel name. Once joined: 1. Messages are sent using the `postMessage()` method 2. Messages are received through the `onmessage` event handler 3. Contexts can leave the channel using the `close()` method ## Implementing the Plugin ### Store Configuration To use our plugin, stores need to opt-in to state sharing through configuration: ```ts twoslash export const useCounterStore = defineStore( 'counter', () => { const count = ref(0) const doubleCount = computed(() => count.value * 2) function increment() { count.value++ } return { count, doubleCount, increment } }, { share: { enable: true, initialize: true, }, }, ) ``` The `share` option enables cross-tab synchronization and controls whether the store should initialize its state from other tabs. ### Plugin Registration `main.ts` Register the plugin when creating your Pinia instance: ```ts twoslash const pinia = createPinia() pinia.use(PiniaSharedState) ``` ### Plugin Implementation `plugin/plugin.ts` Here's our complete plugin implementation with TypeScript support: ```ts twoslash type Serializer = { serialize: (value: T) => string deserialize: (value: string) => T } interface BroadcastMessage { type: 'STATE_UPDATE' | 'SYNC_REQUEST' timestamp?: number state?: string } type PluginOptions = { enable?: boolean initialize?: boolean serializer?: Serializer } export interface StoreOptions extends DefineStoreOptions { share?: PluginOptions } // Add type extension for Pinia declare module 'pinia' { // eslint-disable-next-line @typescript-eslint/no-unused-vars export interface DefineStoreOptionsBase { share?: PluginOptions } } export function PiniaSharedState({ enable = false, initialize = false, serializer = { serialize: JSON.stringify, deserialize: JSON.parse, }, }: PluginOptions = {}) { return ({ store, options }: PiniaPluginContext) => { if (!(options.share?.enable ?? enable)) return const channel = new BroadcastChannel(store.$id) let timestamp = 0 let externalUpdate = false // Initial state sync if (options.share?.initialize ?? initialize) { channel.postMessage({ type: 'SYNC_REQUEST' }) } // State change listener store.$subscribe((_mutation, state) => { if (externalUpdate) return timestamp = Date.now() channel.postMessage({ type: 'STATE_UPDATE', timestamp, state: serializer.serialize(state as T), }) }) // Message handler channel.onmessage = (event: MessageEvent) => { const data = event.data if ( data.type === 'STATE_UPDATE' && data.timestamp && data.timestamp > timestamp && data.state ) { externalUpdate = true timestamp = data.timestamp store.$patch(serializer.deserialize(data.state)) externalUpdate = false } if (data.type === 'SYNC_REQUEST') { channel.postMessage({ type: 'STATE_UPDATE', timestamp, state: serializer.serialize(store.$state as T), }) } } } } ``` The plugin works by: 1. Creating a BroadcastChannel for each store 2. Subscribing to store changes and broadcasting updates 3. Handling incoming messages from other tabs 4. Using timestamps to prevent update cycles 5. Supporting custom serialization for complex state ### Communication Flow Diagram ```mermaid flowchart LR A[User interacts with store in Tab 1] --> B[Store state changes] B --> C[Plugin detects change] C --> D[BroadcastChannel posts STATE_UPDATE] D --> E[Other tabs receive STATE_UPDATE] E --> F[Plugin patches store state in Tab 2] ``` ## Using the Synchronized Store Components can use the synchronized store just like any other Pinia store: ```ts twoslash const counterStore = useCounterStore() // State changes will automatically sync across tabs counterStore.increment() ``` ## Conclusion With this Pinia plugin, we've added cross-tab state synchronization with minimal configuration. The solution is lightweight, type-safe, and leverages the built-in BroadcastChannel API. This pattern is particularly useful for applications where users frequently work across multiple tabs and need a consistent state experience. Remember to consider the following when using this plugin: - Only enable sharing for stores that truly need it - Be mindful of performance with large state objects - Consider custom serialization for complex data structures - Test thoroughly across different browser scenarios ## Future Optimization: Web Workers For applications with heavy cross-tab communication or complex state transformations, consider offloading the BroadcastChannel handling to a Web Worker. This approach can improve performance by: - Moving message processing off the main thread - Handling complex state transformations without blocking UI - Reducing main thread load when syncing large state objects - Buffering and batching state updates for better performance This is particularly beneficial when: - Your application has many tabs open simultaneously - State updates are frequent or computationally intensive - You need to perform validation or transformation on synced data - The application handles large datasets that need to be synced You can find the complete code for this plugin in the [GitHub repository](https://github.com/alexanderop/pluginPiniaTabs). It also has examples of how to use it with Web Workers. --- --- title: The Browser That Speaks 200 Languages: Building an AI Translator Without APIs description: Learn how to build a browser-based translator that works offline and handles 200 languages using Vue and Transformers.js tags: ['vue', 'ai'] --- # The Browser That Speaks 200 Languages: Building an AI Translator Without APIs ## Introduction Most AI translation tools rely on external APIs. This means sending data to servers and paying for each request. But what if you could run translations directly in your browser? This guide shows you how to build a free, offline translator that handles 200 languages using Vue and Transformers.js. ## The Tools - Vue 3 for the interface - Transformers.js to run AI models locally - Web Workers to handle heavy processing - NLLB-200, Meta's translation model ```mermaid --- title: Architecture Overview --- graph LR Frontend[Vue Frontend] Worker[Web Worker] TJS[Transformers.js] Model[NLLB-200 Model] Frontend -->|"Text"| Worker Worker -->|"Initialize"| TJS TJS -->|"Load"| Model Model -->|"Results"| TJS TJS -->|"Stream"| Worker Worker -->|"Translation"| Frontend classDef default fill:#344060,stroke:#AB4B99,color:#EAEDF3 classDef accent fill:#8A337B,stroke:#AB4B99,color:#EAEDF3 class TJS,Model accent ``` ## Building the Translator ![AI Translator](../../assets/images/vue-ai-translate.png) ### 1. Set Up Your Project Create a new Vue project with TypeScript: ```bash npm create vite@latest vue-translator -- --template vue-ts cd vue-translator npm install npm install @huggingface/transformers ``` ### 2. Create the Translation Worker The translation happens in a background process. Create `src/worker/translation.worker.ts`: ```typescript // Singleton pattern for the translation pipeline class MyTranslationPipeline { static task: PipelineType = 'translation'; // We use the distilled model for faster loading and inference static model = 'Xenova/nllb-200-distilled-600M'; static instance: TranslationPipeline | null = null; static async getInstance(progress_callback?: ProgressCallback) { if (!this.instance) { this.instance = await pipeline(this.task, this.model, { progress_callback }) as TranslationPipeline; } return this.instance; } } // Type definitions for worker messages interface TranslationRequest { text: string; src_lang: string; tgt_lang: string; } // Worker message handler self.addEventListener('message', async (event: MessageEvent) => { try { // Initialize the translation pipeline with progress tracking const translator = await MyTranslationPipeline.getInstance(x => { self.postMessage(x); }); // Configure streaming for real-time translation updates const streamer = new TextStreamer(translator.tokenizer, { skip_prompt: true, skip_special_tokens: true, callback_function: (text: string) => { self.postMessage({ status: 'update', output: text }); } }); // Perform the translation const output = await translator(event.data.text, { tgt_lang: event.data.tgt_lang, src_lang: event.data.src_lang, streamer, }); // Send the final result self.postMessage({ status: 'complete', output, }); } catch (error) { self.postMessage({ status: 'error', error: error instanceof Error ? error.message : 'An unknown error occurred' }); } }); ``` ### 3. Build the Interface Create a clean interface with two main components: #### Language Selector (`src/components/LanguageSelector.vue`) ```vue ``` #### Progress Bar (`src/components/ProgressBar.vue`) ```vue ``` ### 4. Put It All Together In your main app file: ```vue