From 76cb9c2a39d477a64824a985ade40507e3bbade1 Mon Sep 17 00:00:00 2001 From: Adam Mathes Date: Fri, 13 Feb 2026 21:34:48 -0800 Subject: feat(vanilla): add testing infrastructure and tests (NK-wjnczv) --- vanilla/coverage/app.js.html | 856 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 856 insertions(+) create mode 100644 vanilla/coverage/app.js.html (limited to 'vanilla/coverage/app.js.html') diff --git a/vanilla/coverage/app.js.html b/vanilla/coverage/app.js.html new file mode 100644 index 0000000..954ba71 --- /dev/null +++ b/vanilla/coverage/app.js.html @@ -0,0 +1,856 @@ + + + + + + Code coverage report for app.js + + + + + + + + + +
+
+

All files app.js

+
+ +
+ 84.91% + Statements + 152/179 +
+ + +
+ 65.26% + Branches + 62/95 +
+ + +
+ 69.23% + Functions + 18/26 +
+ + +
+ 88.66% + Lines + 133/150 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +2581x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +5x +5x +4x +4x +4x +4x +  +1x +1x +1x +1x +  +  +  +  +11x +11x +  +11x +11x +11x +11x +11x +11x +  +11x +5x +  +  +11x +11x +10x +10x +10x +10x +  +1x +1x +1x +  +  +  +  +5x +5x +  +  +5x +5x +5x +  +5x +5x +5x +5x +5x +1x +1x +1x +1x +  +5x +  +5x +5x +5x +5x +5x +1x +1x +1x +1x +  +5x +  +5x +5x +5x +5x +5x +1x +1x +1x +1x +  +5x +  +5x +5x +3x +3x +3x +3x +3x +5x +1x +1x +1x +1x +  +3x +  +  +  +  +  +12x +12x +12x +  +12x +10x +10x +  +  +2x +2x +2x +  +2x +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +2x +  +2x +2x +  +2x +  +  +  +  +1x +1x +1x +  +  +  +  +1x +  +  +1x +1x +1x +  +  +1x +  +1x +  +  +  +  +  +  +  +  +1x +1x +1x +  +  +  +  +1x +  +  +1x +1x +1x +1x +  +  +1x +  +  +1x +1x +1x +1x +1x +1x +  +  +  +  +  +  +1x +  +  +  +  +  +  +  +  +4x +  +4x +3x +3x +  +3x +3x +3x +2x +2x +2x +1x +1x +1x +1x +  +  +  +  +  +  +  + 
document.addEventListener('DOMContentLoaded', () => {
+    fetchFeeds();
+    fetchItems(); // Default to fetching recent items
+ 
+    const searchInput = document.getElementById('search-input');
+    searchInput.addEventListener('keypress', (e) => {
+        if (e.key === 'Enter') {
+            const query = searchInput.value.trim();
+            if (query) {
+                document.getElementById('feed-title').textContent = `Search: ${query}`;
+                document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+                fetchItems(null, null, query);
+            }
+        }
+    });
+});
+ 
+export async function fetchFeeds(apiBase = '') {
+    try {
+        const response = await fetch(`${apiBase}/api/feed/`);
+        Iif (!response.ok) throw new Error('Failed to fetch feeds');
+        const feeds = await response.json();
+        renderFeeds(feeds);
+        return feeds;
+    } catch (err) {
+        console.error(err);
+        const nav = document.getElementById('feeds-nav');
+        Eif (nav) nav.innerHTML = '<div class="error">Error loading feeds</div>';
+        throw err;
+    }
+}
+ 
+export async function fetchItems(feedId = null, filter = null, query = null, apiBase = '') {
+    const listEl = document.getElementById('entries-list');
+    Eif (listEl) listEl.innerHTML = '<div class="loading">Loading items...</div>';
+ 
+    let url = `${apiBase}/api/stream/`;
+    const params = new URLSearchParams();
+    if (feedId) params.append('feed_id', feedId);
+    if (filter === 'unread') params.append('read_filter', 'unread');
+    if (filter === 'starred') params.append('starred', 'true');
+    if (query) params.append('q', query);
+ 
+    if ([...params].length > 0) {
+        url += '?' + params.toString();
+    }
+ 
+    try {
+        const response = await fetch(url);
+        Iif (!response.ok) throw new Error('Failed to fetch items');
+        const items = await response.json();
+        renderItems(items);
+        return items;
+    } catch (err) {
+        console.error(err);
+        Eif (listEl) listEl.innerHTML = '<div class="error">Error loading items</div>';
+        throw err;
+    }
+}
+ 
+export function renderFeeds(feeds) {
+    const nav = document.getElementById('feeds-nav');
+    Iif (!nav) return;
+ 
+    // Clear existing items but keep search container if present
+    const searchContainer = nav.querySelector('.search-container');
+    nav.innerHTML = '';
+    Eif (searchContainer) nav.appendChild(searchContainer);
+ 
+    const allLink = document.createElement('div');
+    allLink.className = 'feed-item';
+    allLink.textContent = 'All Items';
+    allLink.onclick = () => {
+        document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+        allLink.classList.add('active');
+        const title = document.getElementById('feed-title');
+        Eif (title) title.textContent = 'All Items';
+        fetchItems();
+    };
+    nav.appendChild(allLink);
+ 
+    const unreadLink = document.createElement('div');
+    unreadLink.className = 'feed-item';
+    unreadLink.textContent = 'Unread Items';
+    unreadLink.onclick = () => {
+        document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+        unreadLink.classList.add('active');
+        const title = document.getElementById('feed-title');
+        Eif (title) title.textContent = 'Unread Items';
+        fetchItems(null, 'unread');
+    };
+    nav.appendChild(unreadLink);
+ 
+    const starredLink = document.createElement('div');
+    starredLink.className = 'feed-item';
+    starredLink.textContent = 'Starred Items';
+    starredLink.onclick = () => {
+        document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+        starredLink.classList.add('active');
+        const title = document.getElementById('feed-title');
+        Eif (title) title.textContent = 'Starred Items';
+        fetchItems(null, 'starred');
+    };
+    nav.appendChild(starredLink);
+ 
+    Eif (Array.isArray(feeds)) {
+        feeds.forEach(feed => {
+            const div = document.createElement('div');
+            div.className = 'feed-item';
+            div.textContent = feed.title || feed.url;
+            div.title = feed.url;
+            div.onclick = () => {
+                document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+                div.classList.add('active');
+                const title = document.getElementById('feed-title');
+                Eif (title) title.textContent = feed.title;
+                fetchItems(feed.id);
+            };
+            nav.appendChild(div);
+        });
+    }
+}
+ 
+export function renderItems(items) {
+    const list = document.getElementById('entries-list');
+    Iif (!list) return;
+    list.innerHTML = '';
+ 
+    if (!items || items.length === 0) {
+        list.innerHTML = '<div class="empty">No items found.</div>';
+        return;
+    }
+ 
+    items.forEach(item => {
+        const article = document.createElement('article');
+        article.className = 'entry';
+ 
+        const date = new Date(item.published_at || item.created_at).toLocaleString();
+ 
+        article.innerHTML = `
+            <header class="entry-header">
+                <div class="entry-controls">
+                    <button class="btn-star ${item.starred ? 'active' : ''}" data-id="${item.id}" data-starred="${item.starred}">${item.starred ? '★' : '☆'}</button>
+                    <button class="btn-read ${item.read ? 'read' : 'unread'}" data-id="${item.id}" data-read="${item.read}">${item.read ? 'Mark Unread' : 'Mark Read'}</button>
+                </div>
+                <a href="${item.url}" class="entry-title ${item.read ? 'read' : ''}" target="_blank">${item.title}</a>
+                <div class="entry-meta">
+                    ${item.feed ? `<span class="feed-name">${item.feed.title}</span> • ` : ''}
+                    <span class="date">${date}</span>
+                </div>
+            </header>
+            <div class="entry-content">
+                ${item.description || ''}
+            </div>
+        `;
+ 
+        // Add event listeners programmatically to avoid inline onclick with modules
+        const starBtn = article.querySelector('.btn-star');
+        starBtn.onclick = () => toggleStar(item.id, item.starred, starBtn);
+ 
+        const readBtn = article.querySelector('.btn-read');
+        readBtn.onclick = () => toggleRead(item.id, item.read, readBtn);
+ 
+        list.appendChild(article);
+    });
+}
+ 
+export async function toggleStar(id, currentStatus, btn, apiBase = '') {
+    const newStatus = !currentStatus;
+    try {
+        const response = await fetch(`${apiBase}/api/item/${id}`, {
+            method: 'PUT',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ id: id, starred: newStatus })
+        });
+        Iif (!response.ok) throw new Error('Failed to toggle star');
+ 
+        // Update UI
+        btn.textContent = newStatus ? '★' : '☆';
+        btn.classList.toggle('active');
+        btn.onclick = () => toggleStar(id, newStatus, btn, apiBase);
+ 
+        // Update data attributes
+        btn.dataset.starred = newStatus;
+ 
+        return newStatus;
+    } catch (err) {
+        console.error(err);
+        alert('Error toggling star');
+        throw err;
+    }
+}
+ 
+export async function toggleRead(id, currentStatus, btn, apiBase = '') {
+    const newStatus = !currentStatus;
+    try {
+        const response = await fetch(`${apiBase}/api/item/${id}`, {
+            method: 'PUT',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ id: id, read: newStatus })
+        });
+        Iif (!response.ok) throw new Error('Failed to toggle read');
+ 
+        // Update UI
+        btn.textContent = newStatus ? 'Mark Unread' : 'Mark Read';
+        btn.classList.toggle('read');
+        btn.classList.toggle('unread');
+        btn.onclick = () => toggleRead(id, newStatus, btn, apiBase);
+ 
+        // Update data attributes
+        btn.dataset.read = newStatus;
+ 
+        // Find title and dim it if read
+        const header = btn.closest('.entry-header');
+        Eif (header) {
+            const title = header.querySelector('.entry-title');
+            Eif (title) {
+                if (newStatus) {
+                    title.classList.add('read');
+                } else E{
+                    title.classList.remove('read');
+                }
+            }
+        }
+ 
+        return newStatus;
+    } catch (err) {
+        console.error(err);
+        alert('Error toggling read status');
+        throw err;
+    }
+}
+ 
+export function init() {
+    Eif (typeof document !== 'undefined') {
+        // Only run if we're in a browser environment with these elements
+        if (document.getElementById('feeds-nav')) {
+            fetchFeeds();
+            fetchItems();
+ 
+            const searchInput = document.getElementById('search-input');
+            Eif (searchInput) {
+                searchInput.addEventListener('keypress', (e) => {
+                    Eif (e.key === 'Enter') {
+                        const query = searchInput.value.trim();
+                        if (query) {
+                            const title = document.getElementById('feed-title');
+                            Eif (title) title.textContent = `Search: ${query}`;
+                            document.querySelectorAll('.feed-item').forEach(el => el.classList.remove('active'));
+                            fetchItems(null, null, query);
+                        }
+                    }
+                });
+            }
+        }
+    }
+}
+ 
+ +
+
+ + + + + + + + \ No newline at end of file -- cgit v1.2.3