diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-14 10:03:35 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-14 10:03:35 -0800 |
| commit | a4997a5fbc65913b55f2215eb3b868693bd76c51 (patch) | |
| tree | fe303ee7c5e5aba89f1c13766b14556f6e3d2f79 /frontend/coverage/src/components/Settings.tsx.html | |
| parent | 4d058d9ddb34f0e8d384b20d4b9e30f74d349129 (diff) | |
| download | neko-a4997a5fbc65913b55f2215eb3b868693bd76c51.tar.gz neko-a4997a5fbc65913b55f2215eb3b868693bd76c51.tar.bz2 neko-a4997a5fbc65913b55f2215eb3b868693bd76c51.zip | |
test: increase frontend coverage for Settings and improve FeedItem css
Diffstat (limited to 'frontend/coverage/src/components/Settings.tsx.html')
| -rw-r--r-- | frontend/coverage/src/components/Settings.tsx.html | 524 |
1 files changed, 352 insertions, 172 deletions
diff --git a/frontend/coverage/src/components/Settings.tsx.html b/frontend/coverage/src/components/Settings.tsx.html index df6d027..3d8d219 100644 --- a/frontend/coverage/src/components/Settings.tsx.html +++ b/frontend/coverage/src/components/Settings.tsx.html @@ -1,64 +1,68 @@ + <!doctype html> <html lang="en"> - <head> + +<head> <title>Code coverage report for src/components/Settings.tsx</title> <meta charset="utf-8" /> <link rel="stylesheet" href="../../prettify.css" /> <link rel="stylesheet" href="../../base.css" /> <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <style type="text/css"> - .coverage-summary .sorter { - background-image: url(../../sort-arrow-sprite.png); - } + <style type='text/css'> + .coverage-summary .sorter { + background-image: url(../../sort-arrow-sprite.png); + } </style> - </head> - - <body> - <div class="wrapper"> - <div class="pad1"> - <h1> - <a href="../../index.html">All files</a> / - <a href="index.html">src/components</a> Settings.tsx - </h1> - <div class="clearfix"> - <div class="fl pad1y space-right2"> - <span class="strong">75.55% </span> - <span class="quiet">Statements</span> - <span class="fraction">34/45</span> - </div> - - <div class="fl pad1y space-right2"> - <span class="strong">56.25% </span> - <span class="quiet">Branches</span> - <span class="fraction">9/16</span> - </div> - - <div class="fl pad1y space-right2"> - <span class="strong">82.35% </span> - <span class="quiet">Functions</span> - <span class="fraction">14/17</span> - </div> - - <div class="fl pad1y space-right2"> - <span class="strong">84.61% </span> - <span class="quiet">Lines</span> - <span class="fraction">33/39</span> - </div> +</head> + +<body> +<div class='wrapper'> + <div class='pad1'> + <h1><a href="../../index.html">All files</a> / <a href="index.html">src/components</a> Settings.tsx</h1> + <div class='clearfix'> + + <div class='fl pad1y space-right2'> + <span class="strong">56.25% </span> + <span class="quiet">Statements</span> + <span class='fraction'>36/64</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">41.66% </span> + <span class="quiet">Branches</span> + <span class='fraction'>10/24</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">63.63% </span> + <span class="quiet">Functions</span> + <span class='fraction'>14/22</span> + </div> + + + <div class='fl pad1y space-right2'> + <span class="strong">62.5% </span> + <span class="quiet">Lines</span> + <span class='fraction'>35/56</span> + </div> + + </div> <p class="quiet"> - Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, - <em>p</em> or <em>k</em> for the previous block. + Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block. </p> <template id="filterTemplate"> - <div class="quiet"> - Filter: - <input type="search" id="fileSearch" /> - </div> + <div class="quiet"> + Filter: + <input type="search" id="fileSearch"> + </div> </template> - </div> - <div class="status-line medium"></div> - <pre><table class="coverage"> + </div> + <div class='status-line medium'></div> + <pre><table class="coverage"> <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a> <a name='L2'></a><a href='#L2'>2</a> <a name='L3'></a><a href='#L3'>3</a> @@ -180,7 +184,67 @@ <a name='L119'></a><a href='#L119'>119</a> <a name='L120'></a><a href='#L120'>120</a> <a name='L121'></a><a href='#L121'>121</a> -<a name='L122'></a><a href='#L122'>122</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> +<a name='L122'></a><a href='#L122'>122</a> +<a name='L123'></a><a href='#L123'>123</a> +<a name='L124'></a><a href='#L124'>124</a> +<a name='L125'></a><a href='#L125'>125</a> +<a name='L126'></a><a href='#L126'>126</a> +<a name='L127'></a><a href='#L127'>127</a> +<a name='L128'></a><a href='#L128'>128</a> +<a name='L129'></a><a href='#L129'>129</a> +<a name='L130'></a><a href='#L130'>130</a> +<a name='L131'></a><a href='#L131'>131</a> +<a name='L132'></a><a href='#L132'>132</a> +<a name='L133'></a><a href='#L133'>133</a> +<a name='L134'></a><a href='#L134'>134</a> +<a name='L135'></a><a href='#L135'>135</a> +<a name='L136'></a><a href='#L136'>136</a> +<a name='L137'></a><a href='#L137'>137</a> +<a name='L138'></a><a href='#L138'>138</a> +<a name='L139'></a><a href='#L139'>139</a> +<a name='L140'></a><a href='#L140'>140</a> +<a name='L141'></a><a href='#L141'>141</a> +<a name='L142'></a><a href='#L142'>142</a> +<a name='L143'></a><a href='#L143'>143</a> +<a name='L144'></a><a href='#L144'>144</a> +<a name='L145'></a><a href='#L145'>145</a> +<a name='L146'></a><a href='#L146'>146</a> +<a name='L147'></a><a href='#L147'>147</a> +<a name='L148'></a><a href='#L148'>148</a> +<a name='L149'></a><a href='#L149'>149</a> +<a name='L150'></a><a href='#L150'>150</a> +<a name='L151'></a><a href='#L151'>151</a> +<a name='L152'></a><a href='#L152'>152</a> +<a name='L153'></a><a href='#L153'>153</a> +<a name='L154'></a><a href='#L154'>154</a> +<a name='L155'></a><a href='#L155'>155</a> +<a name='L156'></a><a href='#L156'>156</a> +<a name='L157'></a><a href='#L157'>157</a> +<a name='L158'></a><a href='#L158'>158</a> +<a name='L159'></a><a href='#L159'>159</a> +<a name='L160'></a><a href='#L160'>160</a> +<a name='L161'></a><a href='#L161'>161</a> +<a name='L162'></a><a href='#L162'>162</a> +<a name='L163'></a><a href='#L163'>163</a> +<a name='L164'></a><a href='#L164'>164</a> +<a name='L165'></a><a href='#L165'>165</a> +<a name='L166'></a><a href='#L166'>166</a> +<a name='L167'></a><a href='#L167'>167</a> +<a name='L168'></a><a href='#L168'>168</a> +<a name='L169'></a><a href='#L169'>169</a> +<a name='L170'></a><a href='#L170'>170</a> +<a name='L171'></a><a href='#L171'>171</a> +<a name='L172'></a><a href='#L172'>172</a> +<a name='L173'></a><a href='#L173'>173</a> +<a name='L174'></a><a href='#L174'>174</a> +<a name='L175'></a><a href='#L175'>175</a> +<a name='L176'></a><a href='#L176'>176</a> +<a name='L177'></a><a href='#L177'>177</a> +<a name='L178'></a><a href='#L178'>178</a> +<a name='L179'></a><a href='#L179'>179</a> +<a name='L180'></a><a href='#L180'>180</a> +<a name='L181'></a><a href='#L181'>181</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> @@ -191,8 +255,6 @@ <span class="cline-any cline-yes">14x</span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-yes">14x</span> -<span class="cline-any cline-yes">3x</span> -<span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">4x</span> @@ -212,6 +274,10 @@ <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-yes">14x</span> +<span class="cline-any cline-yes">3x</span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-yes">1x</span> <span class="cline-any cline-yes">1x</span> <span class="cline-any cline-neutral"> </span> @@ -254,6 +320,34 @@ <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-yes">14x</span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-yes">14x</span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> @@ -281,6 +375,34 @@ <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-no"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> +<span class="cline-any cline-neutral"> </span> <span class="cline-any cline-yes">5x</span> <span class="cline-any cline-neutral"> </span> <span class="cline-any cline-neutral"> </span> @@ -304,142 +426,200 @@ <span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import React, { useEffect, useState } from 'react'; import type { Feed } from '../types'; import './Settings.css'; +import { apiFetch } from '../utils'; export default function Settings() { - const [feeds, setFeeds] = useState<Feed[]>([]); - const [newFeedUrl, setNewFeedUrl] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState<string | null>(null); + const [feeds, setFeeds] = useState<Feed[]>([]); + const [newFeedUrl, setNewFeedUrl] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); - useEffect(() => { + const [importFile, setImportFile] = useState<File | null>(null); + + const fetchFeeds = () => { + setLoading(true); + apiFetch('/api/feed/') + .then((res) => { + <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) <span class="cstat-no" title="statement not covered" >throw new Error('Failed to fetch feeds');</span> + return res.json(); + }) + .then((data) => { + setFeeds(data); + setLoading(false); + }) + .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) => { +<span class="cstat-no" title="statement not covered" > setError(err.message);</span> +<span class="cstat-no" title="statement not covered" > setLoading(false);</span> + }); + }; + + useEffect(() => { + fetchFeeds(); + }, []); + + const handleAddFeed = (e: React.FormEvent) => { + e.preventDefault(); + <span class="missing-if-branch" title="if path not taken" >I</span>if (!newFeedUrl) <span class="cstat-no" title="statement not covered" >return;</span> + + setLoading(true); + apiFetch('/api/feed/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: newFeedUrl }), + }) + .then((res) => { + <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) <span class="cstat-no" title="statement not covered" >throw new Error('Failed to add feed');</span> + return res.json(); + }) + .then(() => { + setNewFeedUrl(''); fetchFeeds(); - }, []); + }) + .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) => { +<span class="cstat-no" title="statement not covered" > setError(err.message);</span> +<span class="cstat-no" title="statement not covered" > setLoading(false);</span> + }); + }; + + const handleDeleteFeed = (id: number) => { + <span class="missing-if-branch" title="if path not taken" >I</span>if (!globalThis.confirm('Are you sure you want to delete this feed?')) <span class="cstat-no" title="statement not covered" >return;</span> - const fetchFeeds = () => { - setLoading(true); - fetch('/api/feed/') - .then((res) => { - <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) <span class="cstat-no" title="statement not covered" >throw new Error('Failed to fetch feeds');</span> - return res.json(); - }) - .then((data) => { - setFeeds(data); - setLoading(false); - }) - .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) => { -<span class="cstat-no" title="statement not covered" > setError(err.message);</span> -<span class="cstat-no" title="statement not covered" > setLoading(false);</span> - }); - }; + setLoading(true); + apiFetch(`/api/feed/${id}`, { + method: 'DELETE', + }) + .then((res) => { + <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) <span class="cstat-no" title="statement not covered" >throw new Error('Failed to delete feed');</span> + setFeeds(feeds.filter((f) => f._id !== id)); + setLoading(false); + }) + .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) => { +<span class="cstat-no" title="statement not covered" > setError(err.message);</span> +<span class="cstat-no" title="statement not covered" > setLoading(false);</span> + }); + }; - const handleAddFeed = (e: React.FormEvent) => { - e.preventDefault(); - <span class="missing-if-branch" title="if path not taken" >I</span>if (!newFeedUrl) <span class="cstat-no" title="statement not covered" >return;</span> + const handleImport = <span class="fstat-no" title="function not covered" >(e</span>: React.FormEvent) => { +<span class="cstat-no" title="statement not covered" > e.preventDefault();</span> +<span class="cstat-no" title="statement not covered" > if (!importFile) <span class="cstat-no" title="statement not covered" >return;</span></span> - setLoading(true); - fetch('/api/feed/', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: newFeedUrl }), - }) - .then((res) => { - <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) <span class="cstat-no" title="statement not covered" >throw new Error('Failed to add feed');</span> - return res.json(); - }) - .then(() => { - setNewFeedUrl(''); - fetchFeeds(); // Refresh list (or we could append if server returns full feed object) - }) - .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) => { -<span class="cstat-no" title="statement not covered" > setError(err.message);</span> -<span class="cstat-no" title="statement not covered" > setLoading(false);</span> - }); - }; +<span class="cstat-no" title="statement not covered" > setLoading(true);</span> + const formData = <span class="cstat-no" title="statement not covered" >new FormData();</span> +<span class="cstat-no" title="statement not covered" > formData.append('file', importFile);</span> +<span class="cstat-no" title="statement not covered" > formData.append('format', 'opml');</span> - const handleDeleteFeed = (id: number) => { - <span class="missing-if-branch" title="if path not taken" >I</span>if (!globalThis.confirm('Are you sure you want to delete this feed?')) <span class="cstat-no" title="statement not covered" >return;</span> +<span class="cstat-no" title="statement not covered" > apiFetch('/api/import', {</span> + method: 'POST', + body: formData, + }) + .then(<span class="fstat-no" title="function not covered" >(r</span>es) => { +<span class="cstat-no" title="statement not covered" > if (!res.ok) <span class="cstat-no" title="statement not covered" >throw new Error('Failed to import feeds');</span></span> +<span class="cstat-no" title="statement not covered" > return res.json();</span> + }) + .then(<span class="fstat-no" title="function not covered" >() => {</span> +<span class="cstat-no" title="statement not covered" > setImportFile(null);</span> +<span class="cstat-no" title="statement not covered" > fetchFeeds();</span> +<span class="cstat-no" title="statement not covered" > alert('Import successful!');</span> + }) + .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) => { +<span class="cstat-no" title="statement not covered" > setError(err.message);</span> +<span class="cstat-no" title="statement not covered" > setLoading(false);</span> + }); + }; - setLoading(true); - fetch(`/api/feed/${id}`, { - method: 'DELETE', - }) - .then((res) => { - <span class="missing-if-branch" title="if path not taken" >I</span>if (!res.ok) <span class="cstat-no" title="statement not covered" >throw new Error('Failed to delete feed');</span> - setFeeds(feeds.filter((f) => f._id !== id)); - setLoading(false); - }) - .catch(<span class="fstat-no" title="function not covered" >(e</span>rr) => { -<span class="cstat-no" title="statement not covered" > setError(err.message);</span> -<span class="cstat-no" title="statement not covered" > setLoading(false);</span> - }); - }; + return ( + <div className="settings-page"> + <h2>Settings</h2> - return ( - <div className="settings-page"> - <h2>Settings</h2> + <div className="add-feed-section"> + <h3>Add New Feed</h3> + <form onSubmit={handleAddFeed} className="add-feed-form"> + <input + type="url" + value={newFeedUrl} + onChange={(e) => setNewFeedUrl(e.target.value)} + placeholder="https://example.com/feed.xml" + required + className="feed-input" + disabled={loading} + /> + <button type="submit" disabled={loading}> + Add Feed + </button> + </form> + </div> - <div className="add-feed-section"> - <h3>Add New Feed</h3> - <form onSubmit={handleAddFeed} className="add-feed-form"> - <input - type="url" - value={newFeedUrl} - onChange={(e) => setNewFeedUrl(e.target.value)} - placeholder="https://example.com/feed.xml" - required - className="feed-input" - disabled={loading} - /> - <button type="submit" disabled={loading}> - Add Feed - </button> - </form> - {error && <span class="branch-1 cbranch-no" title="branch not covered" ><p className="error-message">{error}</p>}</span> - </div> + <div className="import-export-section"> + <div className="import-section"> + <h3>Import Feeds (OPML)</h3> + <form onSubmit={handleImport} className="import-form"> + <input + type="file" + accept=".opml,.xml,.txt" + onChange={<span class="fstat-no" title="function not covered" >(e</span>) => <span class="cstat-no" title="statement not covered" >setImportFile(e.target.files?.[0] || null)}</span> + className="file-input" + disabled={loading} + /> + <button type="submit" disabled={!importFile || <span class="branch-1 cbranch-no" title="branch not covered" >loading}></span> + Import + </button> + </form> + </div> - <div className="feed-list-section"> - <h3>Manage Feeds</h3> - {loading && <p>Loading...</p>} - <ul className="settings-feed-list"> - {feeds.map((feed) => ( - <li key={feed._id} className="settings-feed-item"> - <div className="feed-info"> - <span className="feed-title">{feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >'(No Title)'}<</span>/span> - <span className="feed-url">{feed.url}</span> - </div> - <button - onClick={() => handleDeleteFeed(feed._id)} - className="delete-btn" - disabled={loading} - title="Delete Feed" - > - Delete - </button> - </li> - ))} - </ul> - </div> + <div className="export-section"> + <h3>Export Feeds</h3> + <div className="export-buttons"> + <a href="/api/export/opml" className="export-btn">OPML</a> + <a href="/api/export/text" className="export-btn">Text</a> + <a href="/api/export/json" className="export-btn">JSON</a> + </div> </div> - ); + </div> + + {error && <span class="branch-1 cbranch-no" title="branch not covered" ><p className="error-message">{error}</p>}</span> + + <div className="feed-list-section"> + <h3>Manage Feeds</h3> + {loading && <p>Loading...</p>} + <ul className="settings-feed-list"> + {feeds.map((feed) => ( + <li key={feed._id} className="settings-feed-item"> + <div className="feed-info"> + <span className="feed-title">{feed.title || <span class="branch-1 cbranch-no" title="branch not covered" >'(No Title)'}<</span>/span> + <span className="feed-url">{feed.url}</span> + </div> + <button + onClick={() => handleDeleteFeed(feed._id)} + className="delete-btn" + disabled={loading} + title="Delete Feed" + > + Delete + </button> + </li> + ))} + </ul> + </div> + </div> + ); } </pre></td></tr></table></pre> - <div class="push"></div> - <!-- for sticky footer --> - </div> - <!-- /wrapper --> - <div class="footer quiet pad2 space-top1 center small"> - Code coverage generated by - <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> - at 2026-02-13T21:49:58.924Z - </div> - <script src="../../prettify.js"></script> - <script> - window.onload = function () { - prettyPrint(); - }; - </script> - <script src="../../sorter.js"></script> - <script src="../../block-navigation.js"></script> - </body> + <div class='push'></div><!-- for sticky footer --> + </div><!-- /wrapper --> + <div class='footer quiet pad2 space-top1 center small'> + Code coverage generated by + <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a> + at 2026-02-14T18:02:09.004Z + </div> + <script src="../../prettify.js"></script> + <script> + window.onload = function () { + prettyPrint(); + }; + </script> + <script src="../../sorter.js"></script> + <script src="../../block-navigation.js"></script> + </body> </html> +
\ No newline at end of file |
