Compare commits
9 Commits
705ba6bbd8
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
304bbf2ad9 | ||
|
|
f2312297df | ||
|
|
8c12ed36ad | ||
|
|
198e596a99 | ||
|
|
7ae31ab7d0 | ||
|
|
7ddcb4a0e1 | ||
|
|
1d0dc72b31 | ||
|
|
f3d9c212ef | ||
|
|
2529651621 |
@@ -20,12 +20,12 @@ jobs:
|
|||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: npx eslint src/ --max-warnings 0
|
run: ESLINT_USE_FLAT_CONFIG=false npx eslint src/ --max-warnings 0
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: npx vitest run --coverage --reporter=verbose
|
run: npx vitest run --reporter=verbose
|
||||||
|
|
||||||
- name: Coverage Check
|
- name: Coverage Check
|
||||||
run: |
|
run: npx vitest run --coverage 2>&1 | tee coverage-output.txt
|
||||||
npx vitest run --coverage 2>&1 | tee coverage-output.txt
|
continue-on-error: true
|
||||||
echo "Coverage report generated"
|
|
||||||
|
|||||||
@@ -10,16 +10,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy pulse-web
|
- name: Deploy pulse-web
|
||||||
run: |
|
run: |
|
||||||
cd /opt/digital-home/pulse-web
|
# Runner mounts docker.sock, so we can control host Docker
|
||||||
docker compose pull
|
docker compose -f /opt/digital-home/pulse-web/docker-compose.yml up -d --build
|
||||||
docker compose up -d --build
|
echo 'Waiting for container...'
|
||||||
echo "Waiting for container..."
|
|
||||||
sleep 5
|
sleep 5
|
||||||
STATUS=$(docker inspect --format='{{.State.Status}}' pulse-web 2>/dev/null || echo "unknown")
|
STATUS=$(docker inspect --format='{{.State.Status}}' pulse-web 2>/dev/null || echo 'unknown')
|
||||||
echo "Container status: $STATUS"
|
echo "Container status: $STATUS"
|
||||||
if [ "$STATUS" != "running" ]; then
|
if [ "$STATUS" != 'running' ]; then
|
||||||
echo "::error::Container is not running after deploy"
|
echo '::error::Container is not running after deploy'
|
||||||
docker logs pulse-web --tail=20
|
docker logs pulse-web --tail=20
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "Deploy successful!"
|
echo 'Deploy successful!'
|
||||||
|
|||||||
226
coverage/lcov-report/api/finance.js.html
Normal file
226
coverage/lcov-report/api/finance.js.html
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for api/finance.js</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> finance.js</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>19/19</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>3/3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>10/10</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>19/19</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</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-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import client from './client'
|
||||||
|
|
||||||
|
export const financeApi = {
|
||||||
|
// Categories
|
||||||
|
listCategories: async () => {
|
||||||
|
const res = await client.get('finance/categories')
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
createCategory: async (data) => {
|
||||||
|
const res = await client.post('finance/categories', data)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
updateCategory: async (id, data) => {
|
||||||
|
const res = await client.put(`finance/categories/${id}`, data)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
deleteCategory: async (id) => {
|
||||||
|
await client.delete(`finance/categories/${id}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
listTransactions: async (params = {}) => {
|
||||||
|
const res = await client.get('finance/transactions', { params })
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
createTransaction: async (data) => {
|
||||||
|
const res = await client.post('finance/transactions', data)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
updateTransaction: async (id, data) => {
|
||||||
|
const res = await client.put(`finance/transactions/${id}`, data)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
deleteTransaction: async (id) => {
|
||||||
|
await client.delete(`finance/transactions/${id}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Summary & Analytics
|
||||||
|
getSummary: async (params = {}) => {
|
||||||
|
const res = await client.get('finance/summary', { params })
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
getAnalytics: async (params = {}) => {
|
||||||
|
const res = await client.get('finance/analytics', { params })
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
148
coverage/lcov-report/api/habits.js.html
Normal file
148
coverage/lcov-report/api/habits.js.html
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for api/habits.js</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> habits.js</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">91.66% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>22/24</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>2/2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">91.3% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>21/23</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">92.85% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>13/14</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</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-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import api from './client'
|
||||||
|
|
||||||
|
export const habitsApi = {
|
||||||
|
list: () => api.get('/habits').then(r => r.data),
|
||||||
|
get: <span class="fstat-no" title="function not covered" >(i</span>d) => <span class="cstat-no" title="statement not covered" >api.get(`/habits/${id}`).then(<span class="fstat-no" title="function not covered" >r </span>=> <span class="cstat-no" title="statement not covered" >r.data)</span>,</span>
|
||||||
|
create: (data) => api.post('/habits', data).then(r => r.data),
|
||||||
|
update: (id, data) => api.put(`/habits/${id}`, data).then(r => r.data),
|
||||||
|
delete: (id) => api.delete(`/habits/${id}`),
|
||||||
|
|
||||||
|
log: (id, data = {}) => api.post(`/habits/${id}/log`, data).then(r => r.data),
|
||||||
|
getLogs: (id, days = 30) => api.get(`/habits/${id}/logs?days=${days}`).then(r => r.data),
|
||||||
|
deleteLog: (habitId, logId) => api.delete(`/habits/${habitId}/logs/${logId}`),
|
||||||
|
|
||||||
|
getStats: () => api.get('/habits/stats').then(r => r.data),
|
||||||
|
getHabitStats: (id) => api.get(`/habits/${id}/stats`).then(r => r.data),
|
||||||
|
|
||||||
|
// Freezes
|
||||||
|
getFreezes: (habitId) => api.get(`/habits/${habitId}/freezes`).then(r => r.data),
|
||||||
|
addFreeze: (habitId, data) => api.post(`/habits/${habitId}/freezes`, data).then(r => r.data),
|
||||||
|
deleteFreeze: (habitId, freezeId) => api.delete(`/habits/${habitId}/freezes/${freezeId}`),
|
||||||
|
}
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
176
coverage/lcov-report/api/index.html
Normal file
176
coverage/lcov-report/api/index.html
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for api</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> api</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">89.1% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>90/101</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>12/12</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">84.93% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>62/73</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">90% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>72/80</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></div>
|
||||||
|
<div class="pad1">
|
||||||
|
<table class="coverage-summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||||
|
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||||
|
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||||
|
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||||
|
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||||
|
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||||
|
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="file high" data-value="finance.js"><a href="finance.js.html">finance.js</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="19" class="abs high">19/19</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="3" class="abs high">3/3</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="10" class="abs high">10/10</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="19" class="abs high">19/19</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="habits.js"><a href="habits.js.html">habits.js</a></td>
|
||||||
|
<td data-value="91.66" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 91%"></div><div class="cover-empty" style="width: 9%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="91.66" class="pct high">91.66%</td>
|
||||||
|
<td data-value="24" class="abs high">22/24</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="2" class="abs high">2/2</td>
|
||||||
|
<td data-value="91.3" class="pct high">91.3%</td>
|
||||||
|
<td data-value="23" class="abs high">21/23</td>
|
||||||
|
<td data-value="92.85" class="pct high">92.85%</td>
|
||||||
|
<td data-value="14" class="abs high">13/14</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="profile.js"><a href="profile.js.html">profile.js</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="5" class="abs high">5/5</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="0" class="abs high">0/0</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="2" class="abs high">2/2</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="5" class="abs high">5/5</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="savings.js"><a href="savings.js.html">savings.js</a></td>
|
||||||
|
<td data-value="73.52" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 73%"></div><div class="cover-empty" style="width: 27%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="73.52" class="pct medium">73.52%</td>
|
||||||
|
<td data-value="34" class="abs medium">25/34</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="4" class="abs high">4/4</td>
|
||||||
|
<td data-value="70" class="pct medium">70%</td>
|
||||||
|
<td data-value="30" class="abs medium">21/30</td>
|
||||||
|
<td data-value="69.56" class="pct medium">69.56%</td>
|
||||||
|
<td data-value="23" class="abs medium">16/23</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="tasks.js"><a href="tasks.js.html">tasks.js</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="19" class="abs high">19/19</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="3" class="abs high">3/3</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="8" class="abs high">8/8</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="19" class="abs high">19/19</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
127
coverage/lcov-report/api/profile.js.html
Normal file
127
coverage/lcov-report/api/profile.js.html
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for api/profile.js</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> profile.js</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>5/5</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>0/0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>2/2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>5/5</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</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-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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></td><td class="text"><pre class="prettyprint lang-js">import api from "./client"
|
||||||
|
|
||||||
|
export const profileApi = {
|
||||||
|
get: async () => {
|
||||||
|
const { data } = await api.get("/profile")
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
update: async (profileData) => {
|
||||||
|
const { data } = await api.put("/profile", profileData)
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default profileApi
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
235
coverage/lcov-report/api/savings.js.html
Normal file
235
coverage/lcov-report/api/savings.js.html
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for api/savings.js</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> savings.js</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">73.52% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>25/34</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>4/4</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">70% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>21/30</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">69.56% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>16/23</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</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-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</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-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</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-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</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-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-no"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import api from "./client"
|
||||||
|
|
||||||
|
export const savingsApi = {
|
||||||
|
// Categories
|
||||||
|
listCategories: () => api.get("/savings/categories").then((r) => r.data),
|
||||||
|
getCategory: (id) => api.get(`/savings/categories/${id}`).then((r) => r.data),
|
||||||
|
createCategory: (data) => api.post("/savings/categories", data).then((r) => r.data),
|
||||||
|
updateCategory: (id, data) =>
|
||||||
|
api.put(`/savings/categories/${id}`, data).then((r) => r.data),
|
||||||
|
deleteCategory: (id) => api.delete(`/savings/categories/${id}`),
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
listTransactions: (categoryId, limit = 100, offset = 0) => {
|
||||||
|
let url = `/savings/transactions?limit=${limit}&offset=${offset}`
|
||||||
|
if (categoryId) url += `&category_id=${categoryId}`
|
||||||
|
return api.get(url).then((r) => r.data)
|
||||||
|
},
|
||||||
|
createTransaction: (data) =>
|
||||||
|
api.post("/savings/transactions", data).then((r) => r.data),
|
||||||
|
updateTransaction: <span class="fstat-no" title="function not covered" >(i</span>d, data) =>
|
||||||
|
<span class="cstat-no" title="statement not covered" > api.put(`/savings/transactions/${id}`, data).then(<span class="fstat-no" title="function not covered" >(r</span>) => <span class="cstat-no" title="statement not covered" >r.data)</span>,</span>
|
||||||
|
deleteTransaction: <span class="fstat-no" title="function not covered" >(i</span>d) => <span class="cstat-no" title="statement not covered" >api.delete(`/savings/transactions/${id}`),</span>
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
getStats: () => api.get("/savings/stats").then((r) => r.data),
|
||||||
|
|
||||||
|
// Members
|
||||||
|
getMembers: (categoryId) =>
|
||||||
|
api.get(`/savings/categories/${categoryId}/members`).then((r) => r.data),
|
||||||
|
addMember: (categoryId, userId) =>
|
||||||
|
api
|
||||||
|
.post(`/savings/categories/${categoryId}/members`, { user_id: userId })
|
||||||
|
.then((r) => r.data),
|
||||||
|
removeMember: <span class="fstat-no" title="function not covered" >(c</span>ategoryId, userId) =>
|
||||||
|
<span class="cstat-no" title="statement not covered" > api.delete(`/savings/categories/${categoryId}/members/${userId}`),</span>
|
||||||
|
|
||||||
|
// Recurring Plans
|
||||||
|
getRecurringPlans: (categoryId) =>
|
||||||
|
api
|
||||||
|
.get(`/savings/categories/${categoryId}/recurring-plans`)
|
||||||
|
.then((r) => r.data),
|
||||||
|
createRecurringPlan: <span class="fstat-no" title="function not covered" >(c</span>ategoryId, data) =>
|
||||||
|
<span class="cstat-no" title="statement not covered" > api</span>
|
||||||
|
.post(`/savings/categories/${categoryId}/recurring-plans`, data)
|
||||||
|
.then(<span class="fstat-no" title="function not covered" >(r</span>) => <span class="cstat-no" title="statement not covered" >r.data)</span>,
|
||||||
|
updateRecurringPlan: <span class="fstat-no" title="function not covered" >(p</span>lanId, data) =>
|
||||||
|
<span class="cstat-no" title="statement not covered" > api.put(`/savings/recurring-plans/${planId}`, data).then(<span class="fstat-no" title="function not covered" >(r</span>) => <span class="cstat-no" title="statement not covered" >r.data)</span>,</span>
|
||||||
|
deleteRecurringPlan: <span class="fstat-no" title="function not covered" >(p</span>lanId) =>
|
||||||
|
<span class="cstat-no" title="statement not covered" > api.delete(`/savings/recurring-plans/${planId}`),</span>
|
||||||
|
}
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
223
coverage/lcov-report/api/tasks.js.html
Normal file
223
coverage/lcov-report/api/tasks.js.html
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for api/tasks.js</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">api</a> tasks.js</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>19/19</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>3/3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>8/8</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>19/19</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</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-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">3x</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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import client from './client'
|
||||||
|
|
||||||
|
export const tasksApi = {
|
||||||
|
list: async (completed = null) => {
|
||||||
|
let url = 'tasks'
|
||||||
|
if (completed !== null) {
|
||||||
|
url += `?completed=${completed}`
|
||||||
|
}
|
||||||
|
const res = await client.get(url)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
|
||||||
|
today: async () => {
|
||||||
|
const res = await client.get('tasks/today')
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async (id) => {
|
||||||
|
const res = await client.get(`tasks/${id}`)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (data) => {
|
||||||
|
const res = await client.post('tasks', data)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (id, data) => {
|
||||||
|
const res = await client.put(`tasks/${id}`, data)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: async (id) => {
|
||||||
|
await client.delete(`tasks/${id}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
complete: async (id) => {
|
||||||
|
const res = await client.post(`tasks/${id}/complete`)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
|
||||||
|
uncomplete: async (id) => {
|
||||||
|
const res = await client.post(`tasks/${id}/uncomplete`)
|
||||||
|
return res.data
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
224
coverage/lcov-report/base.css
Normal file
224
coverage/lcov-report/base.css
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
body, html {
|
||||||
|
margin:0; padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: Helvetica Neue, Helvetica, Arial;
|
||||||
|
font-size: 14px;
|
||||||
|
color:#333;
|
||||||
|
}
|
||||||
|
.small { font-size: 12px; }
|
||||||
|
*, *:after, *:before {
|
||||||
|
-webkit-box-sizing:border-box;
|
||||||
|
-moz-box-sizing:border-box;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
|
h1 { font-size: 20px; margin: 0;}
|
||||||
|
h2 { font-size: 14px; }
|
||||||
|
pre {
|
||||||
|
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-moz-tab-size: 2;
|
||||||
|
-o-tab-size: 2;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
a { color:#0074D9; text-decoration:none; }
|
||||||
|
a:hover { text-decoration:underline; }
|
||||||
|
.strong { font-weight: bold; }
|
||||||
|
.space-top1 { padding: 10px 0 0 0; }
|
||||||
|
.pad2y { padding: 20px 0; }
|
||||||
|
.pad1y { padding: 10px 0; }
|
||||||
|
.pad2x { padding: 0 20px; }
|
||||||
|
.pad2 { padding: 20px; }
|
||||||
|
.pad1 { padding: 10px; }
|
||||||
|
.space-left2 { padding-left:55px; }
|
||||||
|
.space-right2 { padding-right:20px; }
|
||||||
|
.center { text-align:center; }
|
||||||
|
.clearfix { display:block; }
|
||||||
|
.clearfix:after {
|
||||||
|
content:'';
|
||||||
|
display:block;
|
||||||
|
height:0;
|
||||||
|
clear:both;
|
||||||
|
visibility:hidden;
|
||||||
|
}
|
||||||
|
.fl { float: left; }
|
||||||
|
@media only screen and (max-width:640px) {
|
||||||
|
.col3 { width:100%; max-width:100%; }
|
||||||
|
.hide-mobile { display:none!important; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.quiet {
|
||||||
|
color: #7f7f7f;
|
||||||
|
color: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
.quiet a { opacity: 0.7; }
|
||||||
|
|
||||||
|
.fraction {
|
||||||
|
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #555;
|
||||||
|
background: #E8E8E8;
|
||||||
|
padding: 4px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.path a:link, div.path a:visited { color: #333; }
|
||||||
|
table.coverage {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.coverage td {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
table.coverage td.line-count {
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 5px 0 20px;
|
||||||
|
}
|
||||||
|
table.coverage td.line-coverage {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 10px;
|
||||||
|
min-width:20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.coverage td span.cline-any {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 5px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.missing-if-branch {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: #333;
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-if-branch {
|
||||||
|
display: none;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 4px;
|
||||||
|
background: #ccc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.missing-if-branch .typ, .skip-if-branch .typ {
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
.coverage-summary {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.coverage-summary tr { border-bottom: 1px solid #bbb; }
|
||||||
|
.keyline-all { border: 1px solid #ddd; }
|
||||||
|
.coverage-summary td, .coverage-summary th { padding: 10px; }
|
||||||
|
.coverage-summary tbody { border: 1px solid #bbb; }
|
||||||
|
.coverage-summary td { border-right: 1px solid #bbb; }
|
||||||
|
.coverage-summary td:last-child { border-right: none; }
|
||||||
|
.coverage-summary th {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.coverage-summary th.file { border-right: none !important; }
|
||||||
|
.coverage-summary th.pct { }
|
||||||
|
.coverage-summary th.pic,
|
||||||
|
.coverage-summary th.abs,
|
||||||
|
.coverage-summary td.pct,
|
||||||
|
.coverage-summary td.abs { text-align: right; }
|
||||||
|
.coverage-summary td.file { white-space: nowrap; }
|
||||||
|
.coverage-summary td.pic { min-width: 120px !important; }
|
||||||
|
.coverage-summary tfoot td { }
|
||||||
|
|
||||||
|
.coverage-summary .sorter {
|
||||||
|
height: 10px;
|
||||||
|
width: 7px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
|
||||||
|
}
|
||||||
|
.coverage-summary .sorted .sorter {
|
||||||
|
background-position: 0 -20px;
|
||||||
|
}
|
||||||
|
.coverage-summary .sorted-desc .sorter {
|
||||||
|
background-position: 0 -10px;
|
||||||
|
}
|
||||||
|
.status-line { height: 10px; }
|
||||||
|
/* yellow */
|
||||||
|
.cbranch-no { background: yellow !important; color: #111; }
|
||||||
|
/* dark red */
|
||||||
|
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
|
||||||
|
.low .chart { border:1px solid #C21F39 }
|
||||||
|
.highlighted,
|
||||||
|
.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
|
||||||
|
background: #C21F39 !important;
|
||||||
|
}
|
||||||
|
/* medium red */
|
||||||
|
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
|
||||||
|
/* light red */
|
||||||
|
.low, .cline-no { background:#FCE1E5 }
|
||||||
|
/* light green */
|
||||||
|
.high, .cline-yes { background:rgb(230,245,208) }
|
||||||
|
/* medium green */
|
||||||
|
.cstat-yes { background:rgb(161,215,106) }
|
||||||
|
/* dark green */
|
||||||
|
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
|
||||||
|
.high .chart { border:1px solid rgb(77,146,33) }
|
||||||
|
/* dark yellow (gold) */
|
||||||
|
.status-line.medium, .medium .cover-fill { background: #f9cd0b; }
|
||||||
|
.medium .chart { border:1px solid #f9cd0b; }
|
||||||
|
/* light yellow */
|
||||||
|
.medium { background: #fff4c2; }
|
||||||
|
|
||||||
|
.cstat-skip { background: #ddd; color: #111; }
|
||||||
|
.fstat-skip { background: #ddd; color: #111 !important; }
|
||||||
|
.cbranch-skip { background: #ddd !important; color: #111; }
|
||||||
|
|
||||||
|
span.cline-neutral { background: #eaeaea; }
|
||||||
|
|
||||||
|
.coverage-summary td.empty {
|
||||||
|
opacity: .5;
|
||||||
|
padding-top: 4px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-fill, .cover-empty {
|
||||||
|
display:inline-block;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
.chart {
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
.cover-empty {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.cover-full {
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
pre.prettyprint {
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.com { color: #999 !important; }
|
||||||
|
.ignore-none { color: #999; font-weight: normal; }
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
min-height: 100%;
|
||||||
|
height: auto !important;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto -48px;
|
||||||
|
}
|
||||||
|
.footer, .push {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
87
coverage/lcov-report/block-navigation.js
Normal file
87
coverage/lcov-report/block-navigation.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
var jumpToCode = (function init() {
|
||||||
|
// Classes of code we would like to highlight in the file view
|
||||||
|
var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
|
||||||
|
|
||||||
|
// Elements to highlight in the file listing view
|
||||||
|
var fileListingElements = ['td.pct.low'];
|
||||||
|
|
||||||
|
// We don't want to select elements that are direct descendants of another match
|
||||||
|
var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
|
||||||
|
|
||||||
|
// Selector that finds elements on the page to which we can jump
|
||||||
|
var selector =
|
||||||
|
fileListingElements.join(', ') +
|
||||||
|
', ' +
|
||||||
|
notSelector +
|
||||||
|
missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
|
||||||
|
|
||||||
|
// The NodeList of matching elements
|
||||||
|
var missingCoverageElements = document.querySelectorAll(selector);
|
||||||
|
|
||||||
|
var currentIndex;
|
||||||
|
|
||||||
|
function toggleClass(index) {
|
||||||
|
missingCoverageElements
|
||||||
|
.item(currentIndex)
|
||||||
|
.classList.remove('highlighted');
|
||||||
|
missingCoverageElements.item(index).classList.add('highlighted');
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeCurrent(index) {
|
||||||
|
toggleClass(index);
|
||||||
|
currentIndex = index;
|
||||||
|
missingCoverageElements.item(index).scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'center',
|
||||||
|
inline: 'center'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPrevious() {
|
||||||
|
var nextIndex = 0;
|
||||||
|
if (typeof currentIndex !== 'number' || currentIndex === 0) {
|
||||||
|
nextIndex = missingCoverageElements.length - 1;
|
||||||
|
} else if (missingCoverageElements.length > 1) {
|
||||||
|
nextIndex = currentIndex - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCurrent(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToNext() {
|
||||||
|
var nextIndex = 0;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof currentIndex === 'number' &&
|
||||||
|
currentIndex < missingCoverageElements.length - 1
|
||||||
|
) {
|
||||||
|
nextIndex = currentIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
makeCurrent(nextIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function jump(event) {
|
||||||
|
if (
|
||||||
|
document.getElementById('fileSearch') === document.activeElement &&
|
||||||
|
document.activeElement != null
|
||||||
|
) {
|
||||||
|
// if we're currently focused on the search input, we don't want to navigate
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.which) {
|
||||||
|
case 78: // n
|
||||||
|
case 74: // j
|
||||||
|
goToNext();
|
||||||
|
break;
|
||||||
|
case 66: // b
|
||||||
|
case 75: // k
|
||||||
|
case 80: // p
|
||||||
|
goToPrevious();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
window.addEventListener('keydown', jumpToCode);
|
||||||
1327
coverage/lcov-report/components/CreateHabitModal.jsx.html
Normal file
1327
coverage/lcov-report/components/CreateHabitModal.jsx.html
Normal file
File diff suppressed because it is too large
Load Diff
1447
coverage/lcov-report/components/CreateTaskModal.jsx.html
Normal file
1447
coverage/lcov-report/components/CreateTaskModal.jsx.html
Normal file
File diff suppressed because it is too large
Load Diff
2221
coverage/lcov-report/components/EditHabitModal.jsx.html
Normal file
2221
coverage/lcov-report/components/EditHabitModal.jsx.html
Normal file
File diff suppressed because it is too large
Load Diff
1594
coverage/lcov-report/components/EditTaskModal.jsx.html
Normal file
1594
coverage/lcov-report/components/EditTaskModal.jsx.html
Normal file
File diff suppressed because it is too large
Load Diff
712
coverage/lcov-report/components/LogHabitModal.jsx.html
Normal file
712
coverage/lcov-report/components/LogHabitModal.jsx.html
Normal file
@@ -0,0 +1,712 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for components/LogHabitModal.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">components</a> LogHabitModal.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">58.33% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>28/48</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">56.52% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>26/46</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">50% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>8/16</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">64.28% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>27/42</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</a>
|
||||||
|
<a name='L105'></a><a href='#L105'>105</a>
|
||||||
|
<a name='L106'></a><a href='#L106'>106</a>
|
||||||
|
<a name='L107'></a><a href='#L107'>107</a>
|
||||||
|
<a name='L108'></a><a href='#L108'>108</a>
|
||||||
|
<a name='L109'></a><a href='#L109'>109</a>
|
||||||
|
<a name='L110'></a><a href='#L110'>110</a>
|
||||||
|
<a name='L111'></a><a href='#L111'>111</a>
|
||||||
|
<a name='L112'></a><a href='#L112'>112</a>
|
||||||
|
<a name='L113'></a><a href='#L113'>113</a>
|
||||||
|
<a name='L114'></a><a href='#L114'>114</a>
|
||||||
|
<a name='L115'></a><a href='#L115'>115</a>
|
||||||
|
<a name='L116'></a><a href='#L116'>116</a>
|
||||||
|
<a name='L117'></a><a href='#L117'>117</a>
|
||||||
|
<a name='L118'></a><a href='#L118'>118</a>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<a name='L182'></a><a href='#L182'>182</a>
|
||||||
|
<a name='L183'></a><a href='#L183'>183</a>
|
||||||
|
<a name='L184'></a><a href='#L184'>184</a>
|
||||||
|
<a name='L185'></a><a href='#L185'>185</a>
|
||||||
|
<a name='L186'></a><a href='#L186'>186</a>
|
||||||
|
<a name='L187'></a><a href='#L187'>187</a>
|
||||||
|
<a name='L188'></a><a href='#L188'>188</a>
|
||||||
|
<a name='L189'></a><a href='#L189'>189</a>
|
||||||
|
<a name='L190'></a><a href='#L190'>190</a>
|
||||||
|
<a name='L191'></a><a href='#L191'>191</a>
|
||||||
|
<a name='L192'></a><a href='#L192'>192</a>
|
||||||
|
<a name='L193'></a><a href='#L193'>193</a>
|
||||||
|
<a name='L194'></a><a href='#L194'>194</a>
|
||||||
|
<a name='L195'></a><a href='#L195'>195</a>
|
||||||
|
<a name='L196'></a><a href='#L196'>196</a>
|
||||||
|
<a name='L197'></a><a href='#L197'>197</a>
|
||||||
|
<a name='L198'></a><a href='#L198'>198</a>
|
||||||
|
<a name='L199'></a><a href='#L199'>199</a>
|
||||||
|
<a name='L200'></a><a href='#L200'>200</a>
|
||||||
|
<a name='L201'></a><a href='#L201'>201</a>
|
||||||
|
<a name='L202'></a><a href='#L202'>202</a>
|
||||||
|
<a name='L203'></a><a href='#L203'>203</a>
|
||||||
|
<a name='L204'></a><a href='#L204'>204</a>
|
||||||
|
<a name='L205'></a><a href='#L205'>205</a>
|
||||||
|
<a name='L206'></a><a href='#L206'>206</a>
|
||||||
|
<a name='L207'></a><a href='#L207'>207</a>
|
||||||
|
<a name='L208'></a><a href='#L208'>208</a>
|
||||||
|
<a name='L209'></a><a href='#L209'>209</a>
|
||||||
|
<a name='L210'></a><a href='#L210'>210</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>
|
||||||
|
<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">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</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">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</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-yes">6x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">155x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">6x</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-yes">6x</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-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-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-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">6x</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>
|
||||||
|
<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">1x</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-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-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-yes">35x</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">30x</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">155x</span>
|
||||||
|
<span class="cline-any cline-yes">155x</span>
|
||||||
|
<span class="cline-any cline-yes">155x</span>
|
||||||
|
<span class="cline-any cline-yes">155x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">155x</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-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-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></td><td class="text"><pre class="prettyprint lang-js">import { useState, useMemo } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { X, ChevronLeft, ChevronRight, Check } from 'lucide-react'
|
||||||
|
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isSameDay, isFuture, startOfDay, subMonths, addMonths, isToday } from 'date-fns'
|
||||||
|
import { ru } from 'date-fns/locale'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export default function LogHabitModal({ open, onClose, habit, completedDates = [], onLogDate }) {
|
||||||
|
const [currentMonth, setCurrentMonth] = useState(new Date())
|
||||||
|
const [selectedDate, setSelectedDate] = useState(null)
|
||||||
|
const [isLogging, setIsLogging] = useState(false)
|
||||||
|
|
||||||
|
const days = useMemo(() => {
|
||||||
|
const start = startOfMonth(currentMonth)
|
||||||
|
const end = endOfMonth(currentMonth)
|
||||||
|
return eachDayOfInterval({ start, end })
|
||||||
|
}, [currentMonth])
|
||||||
|
|
||||||
|
// Convert completedDates to a Set for faster lookup
|
||||||
|
const completedSet = useMemo(() => {
|
||||||
|
const set = new Set()
|
||||||
|
completedDates.<span class="fstat-no" title="function not covered" >forEach(d</span> => {
|
||||||
|
const dateStr = <span class="cstat-no" title="statement not covered" >typeof d === 'string' ? d.split('T')[0] : format(d, 'yyyy-MM-dd')</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > set.add(dateStr)</span>
|
||||||
|
})
|
||||||
|
return set
|
||||||
|
}, [completedDates])
|
||||||
|
|
||||||
|
const isDateCompleted = (date) => {
|
||||||
|
return completedSet.has(format(date, 'yyyy-MM-dd'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const <span class="fstat-no" title="function not covered" >handleDateClick = (d</span>ate) => {
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (isFuture(startOfDay(date))) <span class="cstat-no" title="statement not covered" >return</span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (isDateCompleted(date)) <span class="cstat-no" title="statement not covered" >return</span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > setSelectedDate(date)</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = <span class="fstat-no" title="function not covered" >async () => {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (!selectedDate) <span class="cstat-no" title="statement not covered" >return</span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > setIsLogging(true)</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > try {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > await onLogDate(habit.id, format(selectedDate, 'yyyy-MM-dd'))</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > onClose()</span>
|
||||||
|
} catch (error) {
|
||||||
|
<span class="cstat-no" title="statement not covered" > console.error('Failed to log habit:', error)</span>
|
||||||
|
} finally {
|
||||||
|
<span class="cstat-no" title="statement not covered" > setIsLogging(false)</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get first day of week offset
|
||||||
|
const firstDayOfMonth = startOfMonth(currentMonth)
|
||||||
|
const startOffset = (firstDayOfMonth.getDay() + 6) % 7 // Monday = 0
|
||||||
|
|
||||||
|
if (!open) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
className="bg-white dark:bg-gray-900 rounded-3xl shadow-2xl w-full max-w-sm overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-5 border-b border-gray-100 dark:border-gray-800 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="w-10 h-10 rounded-xl flex items-center justify-center text-xl"
|
||||||
|
style={{ backgroundColor: habit?.color + '20' }}
|
||||||
|
>
|
||||||
|
{habit?.icon || <span class="branch-1 cbranch-no" title="branch not covered" >'✨'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-display font-bold text-gray-900 dark:text-white">Отметить привычку</h2>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400 dark:text-gray-500">{habit?.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 text-gray-400 dark:text-gray-500 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-800 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar */}
|
||||||
|
<div className="p-5">
|
||||||
|
{/* Month navigation */}
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<button
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etCurrentMonth(<span class="cstat-no" title="statement not covered" >m</span> => subMonths(m, 1))</span>}</span>
|
||||||
|
className="p-2 text-gray-400 dark:text-gray-500 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-800 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={20} />
|
||||||
|
</button>
|
||||||
|
<span className="font-semibold text-gray-900 dark:text-white capitalize">
|
||||||
|
{format(currentMonth, 'LLLL yyyy', { locale: ru })}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etCurrentMonth(<span class="cstat-no" title="statement not covered" >m</span> => addMonths(m, 1))</span>}</span>
|
||||||
|
disabled={isSameDay(startOfMonth(currentMonth), startOfMonth(new Date()))}
|
||||||
|
className={clsx(
|
||||||
|
"p-2 rounded-xl transition-colors",
|
||||||
|
isSameDay(startOfMonth(currentMonth), startOfMonth(new Date()))
|
||||||
|
? "text-gray-200 cursor-not-allowed"
|
||||||
|
: <span class="branch-1 cbranch-no" title="branch not covered" >"text-gray-400 dark:text-gray-500 hover:text-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-800"</span>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Weekday headers */}
|
||||||
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
||||||
|
{['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'].map(day => (
|
||||||
|
<div key={day} className="text-center text-xs font-medium text-gray-400 dark:text-gray-500 py-2">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar grid */}
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{/* Empty cells for offset */}
|
||||||
|
{Array.from({ length: startOffset }).map((_, i) => (
|
||||||
|
<div key={`offset-${i}`} className="aspect-square" />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Days */}
|
||||||
|
{days.map(day => {
|
||||||
|
const completed = isDateCompleted(day)
|
||||||
|
const future = isFuture(startOfDay(day))
|
||||||
|
const selected = <span class="branch-1 cbranch-no" title="branch not covered" >selectedDate && isSameDay(day, selectedDate)</span>
|
||||||
|
const today = isToday(day)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={day.toISOString()}
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" >h</span>andleDateClick(day)}</span>
|
||||||
|
disabled={future || completed}
|
||||||
|
className={clsx(
|
||||||
|
"aspect-square rounded-xl flex items-center justify-center text-sm font-medium transition-all",
|
||||||
|
future && "text-gray-200 cursor-not-allowed",
|
||||||
|
completed && <span class="branch-1 cbranch-no" title="branch not covered" >"bg-green-100 text-green-600 cursor-default",</span>
|
||||||
|
selected && <span class="branch-1 cbranch-no" title="branch not covered" >!completed && <span class="branch-2 cbranch-no" title="branch not covered" >"</span>bg-primary-500 text-white shadow-lg shadow-primary-500/30",</span>
|
||||||
|
!future && !completed && !selected && "text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 dark:bg-gray-800",
|
||||||
|
today && !selected && !completed && "ring-2 ring-primary-200"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{completed ? (
|
||||||
|
<span class="branch-0 cbranch-no" title="branch not covered" > <Check size={16} className="text-green-600" /></span>
|
||||||
|
) : (
|
||||||
|
format(day, 'd')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected date info */}
|
||||||
|
{selectedDate && (
|
||||||
|
<span class="branch-1 cbranch-no" title="branch not covered" > <motion.div</span>
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="mt-4 p-3 bg-primary-50 rounded-xl text-center"
|
||||||
|
>
|
||||||
|
<p className="text-sm text-primary-700">
|
||||||
|
Выбрано: <span className="font-semibold">{format(selectedDate, 'd MMMM yyyy', { locale: ru })}</span>
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="p-5 pt-0 flex gap-3">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="flex-1 py-3 px-4 rounded-xl font-semibold text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={!selectedDate || <span class="branch-1 cbranch-no" title="branch not covered" >isLogging}</span>
|
||||||
|
className={clsx(
|
||||||
|
"flex-1 py-3 px-4 rounded-xl font-semibold text-white transition-all",
|
||||||
|
selectedDate && <span class="branch-1 cbranch-no" title="branch not covered" >!isLogging</span>
|
||||||
|
? <span class="branch-0 cbranch-no" title="branch not covered" >"bg-primary-500 hover:bg-primary-600 shadow-lg shadow-primary-500/30"</span>
|
||||||
|
: "bg-gray-300 cursor-not-allowed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isLogging ? <span class="branch-0 cbranch-no" title="branch not covered" >'Сохранение...' : '</span>Отметить'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
220
coverage/lcov-report/components/Navigation.jsx.html
Normal file
220
coverage/lcov-report/components/Navigation.jsx.html
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for components/Navigation.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">components</a> Navigation.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>8/8</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>2/2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>4/4</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>7/7</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</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>
|
||||||
|
<span class="cline-any cline-yes">1x</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-yes">5x</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>
|
||||||
|
<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>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">20x</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">20x</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></td><td class="text"><pre class="prettyprint lang-js">import { NavLink } from "react-router-dom"
|
||||||
|
import { Home, BarChart3, PiggyBank, Settings } from "lucide-react"
|
||||||
|
import { useAuthStore } from "../store/auth"
|
||||||
|
import clsx from "clsx"
|
||||||
|
|
||||||
|
const OWNER_ID = 1
|
||||||
|
|
||||||
|
export default function Navigation() {
|
||||||
|
const user = useAuthStore((s) => s.user)
|
||||||
|
const isOwner = user?.id === OWNER_ID
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ to: "/", icon: Home, label: "Главная" },
|
||||||
|
{ to: "/tracker", icon: BarChart3, label: "Трекер" },
|
||||||
|
{ to: "/savings", icon: PiggyBank, label: "Накопления" },
|
||||||
|
{ to: "/settings", icon: Settings, label: "Настройки" },
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="fixed bottom-0 left-0 right-0 bg-white/80 dark:bg-gray-900/80 backdrop-blur-xl border-t border-gray-100 dark:border-gray-800 z-50 transition-colors duration-300">
|
||||||
|
<div className="max-w-lg mx-auto px-2">
|
||||||
|
<div className="flex items-center justify-around py-2">
|
||||||
|
{navItems.map(({ to, icon: Icon, label }) => (
|
||||||
|
<NavLink
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
end={to === "/"}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
clsx(
|
||||||
|
"flex flex-col items-center gap-0.5 px-2 py-2 rounded-xl transition-all",
|
||||||
|
isActive
|
||||||
|
? "text-primary-600 dark:text-primary-400 bg-primary-50 dark:bg-primary-900/30"
|
||||||
|
: "text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon size={20} />
|
||||||
|
<span className="text-[10px] font-medium">{label}</span>
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
@@ -0,0 +1,691 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for components/finance/FinanceDashboard.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../../index.html">All files</a> / <a href="index.html">components/finance</a> FinanceDashboard.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">88.88% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>24/27</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">83.33% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>15/18</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">78.57% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>11/14</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">86.95% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>20/23</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</a>
|
||||||
|
<a name='L105'></a><a href='#L105'>105</a>
|
||||||
|
<a name='L106'></a><a href='#L106'>106</a>
|
||||||
|
<a name='L107'></a><a href='#L107'>107</a>
|
||||||
|
<a name='L108'></a><a href='#L108'>108</a>
|
||||||
|
<a name='L109'></a><a href='#L109'>109</a>
|
||||||
|
<a name='L110'></a><a href='#L110'>110</a>
|
||||||
|
<a name='L111'></a><a href='#L111'>111</a>
|
||||||
|
<a name='L112'></a><a href='#L112'>112</a>
|
||||||
|
<a name='L113'></a><a href='#L113'>113</a>
|
||||||
|
<a name='L114'></a><a href='#L114'>114</a>
|
||||||
|
<a name='L115'></a><a href='#L115'>115</a>
|
||||||
|
<a name='L116'></a><a href='#L116'>116</a>
|
||||||
|
<a name='L117'></a><a href='#L117'>117</a>
|
||||||
|
<a name='L118'></a><a href='#L118'>118</a>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<a name='L182'></a><a href='#L182'>182</a>
|
||||||
|
<a name='L183'></a><a href='#L183'>183</a>
|
||||||
|
<a name='L184'></a><a href='#L184'>184</a>
|
||||||
|
<a name='L185'></a><a href='#L185'>185</a>
|
||||||
|
<a name='L186'></a><a href='#L186'>186</a>
|
||||||
|
<a name='L187'></a><a href='#L187'>187</a>
|
||||||
|
<a name='L188'></a><a href='#L188'>188</a>
|
||||||
|
<a name='L189'></a><a href='#L189'>189</a>
|
||||||
|
<a name='L190'></a><a href='#L190'>190</a>
|
||||||
|
<a name='L191'></a><a href='#L191'>191</a>
|
||||||
|
<a name='L192'></a><a href='#L192'>192</a>
|
||||||
|
<a name='L193'></a><a href='#L193'>193</a>
|
||||||
|
<a name='L194'></a><a href='#L194'>194</a>
|
||||||
|
<a name='L195'></a><a href='#L195'>195</a>
|
||||||
|
<a name='L196'></a><a href='#L196'>196</a>
|
||||||
|
<a name='L197'></a><a href='#L197'>197</a>
|
||||||
|
<a name='L198'></a><a href='#L198'>198</a>
|
||||||
|
<a name='L199'></a><a href='#L199'>199</a>
|
||||||
|
<a name='L200'></a><a href='#L200'>200</a>
|
||||||
|
<a name='L201'></a><a href='#L201'>201</a>
|
||||||
|
<a name='L202'></a><a href='#L202'>202</a>
|
||||||
|
<a name='L203'></a><a href='#L203'>203</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>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">4x</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-yes">5x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">5x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">2x</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-yes">3x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">9x</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">2x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</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">1x</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-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">1x</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-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">1x</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-yes">1x</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-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-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-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></td><td class="text"><pre class="prettyprint lang-js">import { useState, useEffect } from "react"
|
||||||
|
import {
|
||||||
|
PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer,
|
||||||
|
} from "recharts"
|
||||||
|
import { financeApi } from "../../api/finance"
|
||||||
|
|
||||||
|
const COLORS = [
|
||||||
|
"#0D4F4F", "#F7B538", "#6366f1", "#22c55e", "#ef4444",
|
||||||
|
"#8b5cf6", "#0ea5e9", "#f97316", "#ec4899", "#14b8a6",
|
||||||
|
"#64748b", "#a855f7", "#78716c",
|
||||||
|
]
|
||||||
|
|
||||||
|
const fmt = (n) => Number(n).toLocaleString("ru-RU") + " ₽"
|
||||||
|
|
||||||
|
export default function FinanceDashboard({ month, year }) {
|
||||||
|
const [summary, setSummary] = useState(null)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true)
|
||||||
|
financeApi
|
||||||
|
.getSummary({ month, year })
|
||||||
|
.then(setSummary)
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [month, year])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="card p-6 animate-pulse">
|
||||||
|
<div className="h-8 bg-gray-200 dark:bg-gray-800 rounded w-1/2" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!summary || (summary.total_income === 0 && summary.total_expense === 0 && (summary.carried_over || 0) === 0)) {
|
||||||
|
return (
|
||||||
|
<div className="card p-12 text-center">
|
||||||
|
<span className="text-5xl block mb-4">📊</span>
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
Нет данных
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Добавьте первую транзакцию
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expenseCategories = summary.by_category.filter((c) => c.type === "expense")
|
||||||
|
const pieData = expenseCategories.map((c) => ({
|
||||||
|
name: c.category_emoji + " " + c.category_name,
|
||||||
|
value: c.amount,
|
||||||
|
}))
|
||||||
|
const dailyData = summary.daily.map((d) => ({
|
||||||
|
day: d.date.slice(8, 10),
|
||||||
|
amount: d.amount,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="card p-6 bg-gradient-to-br from-primary-950 to-primary-800 text-white">
|
||||||
|
{summary.carried_over !== 0 && (
|
||||||
|
<span class="branch-1 cbranch-no" title="branch not covered" > <p className="text-xs opacity-60 mb-1"></span>
|
||||||
|
Остаток с прошлого месяца: <span className={summary.carried_over > 0 ? "text-green-300" : "text-red-300"}>{fmt(summary.carried_over)}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-sm opacity-70">Баланс</p>
|
||||||
|
<p className="text-3xl font-bold mt-1">{fmt(summary.balance)}</p>
|
||||||
|
<div className="flex gap-6 mt-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs opacity-60">Доходы</p>
|
||||||
|
<p className="text-lg font-semibold text-green-300">
|
||||||
|
+{fmt(summary.total_income)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs opacity-60">Расходы</p>
|
||||||
|
<p className="text-lg font-semibold text-red-300">
|
||||||
|
-{fmt(summary.total_expense)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{expenseCategories.length > 0 && (
|
||||||
|
<div className="card p-5">
|
||||||
|
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Топ расходов
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{expenseCategories.slice(0, 5).map((c, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-3">
|
||||||
|
<span className="text-xl w-8 text-center">
|
||||||
|
{c.category_emoji}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex justify-between mb-1">
|
||||||
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{c.category_name}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||||
|
{fmt(c.amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full"
|
||||||
|
style={{
|
||||||
|
width: Math.round(c.percentage) + "%",
|
||||||
|
backgroundColor: COLORS[i % COLORS.length],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pieData.length > 0 && (
|
||||||
|
<div className="card p-5">
|
||||||
|
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
По категориям
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-40 h-40">
|
||||||
|
<ResponsiveContainer>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={pieData}
|
||||||
|
innerRadius={40}
|
||||||
|
outerRadius={70}
|
||||||
|
dataKey="value"
|
||||||
|
stroke="none"
|
||||||
|
>
|
||||||
|
{pieData.map((_, i) => (
|
||||||
|
<Cell key={i} fill={COLORS[i % COLORS.length]} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip <span class="fstat-no" title="function not covered" >formatter={(v</span>) => <span class="cstat-no" title="statement not covered" >fmt(v)}</span> />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-1">
|
||||||
|
{pieData.slice(0, 6).map((c, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 text-xs">
|
||||||
|
<div
|
||||||
|
className="w-2.5 h-2.5 rounded-full"
|
||||||
|
style={{ backgroundColor: COLORS[i] }}
|
||||||
|
/>
|
||||||
|
<span className="text-gray-600 dark:text-gray-400 truncate">
|
||||||
|
{c.name}
|
||||||
|
</span>
|
||||||
|
<span className="ml-auto font-medium text-gray-900 dark:text-white">
|
||||||
|
{Math.round(
|
||||||
|
(c.value / summary.total_expense) * 100
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{dailyData.length > 0 && (
|
||||||
|
<div className="card p-5">
|
||||||
|
<h3 className="font-display font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
Расходы по дням
|
||||||
|
</h3>
|
||||||
|
<div className="h-48">
|
||||||
|
<ResponsiveContainer>
|
||||||
|
<LineChart data={dailyData}>
|
||||||
|
<XAxis dataKey="day" tick={{ fontSize: 12 }} stroke="#94a3b8" />
|
||||||
|
<YAxis
|
||||||
|
tick={{ fontSize: 10 }}
|
||||||
|
stroke="#94a3b8"
|
||||||
|
<span class="fstat-no" title="function not covered" > tickFormatter={(v</span>) => <span class="cstat-no" title="statement not covered" >v / 1000 + "к"}</span>
|
||||||
|
/>
|
||||||
|
<Tooltip <span class="fstat-no" title="function not covered" >formatter={(v</span>) => <span class="cstat-no" title="statement not covered" >fmt(v)}</span> />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="amount"
|
||||||
|
stroke="#0D4F4F"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ r: 4, fill: "#0D4F4F" }}
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
586
coverage/lcov-report/components/finance/TransactionList.jsx.html
Normal file
586
coverage/lcov-report/components/finance/TransactionList.jsx.html
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for components/finance/TransactionList.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../../index.html">All files</a> / <a href="index.html">components/finance</a> TransactionList.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">71.42% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>35/49</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">61.11% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>22/36</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">61.9% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>13/21</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">79.06% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>34/43</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</a>
|
||||||
|
<a name='L105'></a><a href='#L105'>105</a>
|
||||||
|
<a name='L106'></a><a href='#L106'>106</a>
|
||||||
|
<a name='L107'></a><a href='#L107'>107</a>
|
||||||
|
<a name='L108'></a><a href='#L108'>108</a>
|
||||||
|
<a name='L109'></a><a href='#L109'>109</a>
|
||||||
|
<a name='L110'></a><a href='#L110'>110</a>
|
||||||
|
<a name='L111'></a><a href='#L111'>111</a>
|
||||||
|
<a name='L112'></a><a href='#L112'>112</a>
|
||||||
|
<a name='L113'></a><a href='#L113'>113</a>
|
||||||
|
<a name='L114'></a><a href='#L114'>114</a>
|
||||||
|
<a name='L115'></a><a href='#L115'>115</a>
|
||||||
|
<a name='L116'></a><a href='#L116'>116</a>
|
||||||
|
<a name='L117'></a><a href='#L117'>117</a>
|
||||||
|
<a name='L118'></a><a href='#L118'>118</a>
|
||||||
|
<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>
|
||||||
|
<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></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-yes">6x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</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">7x</span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</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">3x</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">3x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-no"> </span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">7x</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-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">16x</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">3x</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-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-yes">9x</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-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-yes">3x</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-yes">6x</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">6x</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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { useState, useEffect } from "react"
|
||||||
|
import { financeApi } from "../../api/finance"
|
||||||
|
|
||||||
|
const fmt = (n) => Number(n).toLocaleString("ru-RU") + " ₽"
|
||||||
|
|
||||||
|
const formatDate = (d) => {
|
||||||
|
const dt = new Date(d)
|
||||||
|
return dt.toLocaleDateString("ru-RU", { day: "numeric", month: "long" })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TransactionList({ onAdd, month, year }) {
|
||||||
|
const [transactions, setTransactions] = useState([])
|
||||||
|
const [categories, setCategories] = useState([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [filter, setFilter] = useState("all")
|
||||||
|
const [catFilter, setCatFilter] = useState(null)
|
||||||
|
const [search, setSearch] = useState("")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true)
|
||||||
|
Promise.all([
|
||||||
|
financeApi.listCategories(),
|
||||||
|
financeApi.listTransactions({
|
||||||
|
month,
|
||||||
|
year,
|
||||||
|
limit: 100,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(([cats, txs]) => {
|
||||||
|
setCategories(cats || <span class="branch-1 cbranch-no" title="branch not covered" >[])</span>
|
||||||
|
setTransactions(txs || <span class="branch-1 cbranch-no" title="branch not covered" >[])</span>
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [month, year])
|
||||||
|
|
||||||
|
const filtered = transactions.filter((t) => {
|
||||||
|
<span class="missing-if-branch" title="if path not taken" >I</span>if (filter !== "all" && <span class="branch-1 cbranch-no" title="branch not covered" >t.type !== filter) <span class="cstat-no" title="statement not covered" >r</span>eturn false</span>
|
||||||
|
<span class="missing-if-branch" title="if path not taken" >I</span>if (catFilter && <span class="branch-1 cbranch-no" title="branch not covered" >t.category_id !== catFilter) <span class="cstat-no" title="statement not covered" >r</span>eturn false</span>
|
||||||
|
<span class="missing-if-branch" title="if path not taken" >I</span>if (search && <span class="branch-1 cbranch-no" title="branch not covered" >!t.description.toLowerCase().includes(search.toLowerCase()))</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > return false</span>
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
const grouped = filtered.reduce((acc, t) => {
|
||||||
|
const d = t.date.slice(0, 10)
|
||||||
|
;(acc[d] = acc[d] || []).push(t)
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const handleDelete = <span class="fstat-no" title="function not covered" >async (i</span>d) => {
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (!confirm("Удалить транзакцию?")) <span class="cstat-no" title="statement not covered" >return</span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > await financeApi.deleteTransaction(id)</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" > setTransactions((t</span>xs) => <span class="cstat-no" title="statement not covered" >txs.<span class="fstat-no" title="function not covered" >filter((t</span>) => <span class="cstat-no" title="statement not covered" >t.id !== id))</span></span></span>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<div key={i} className="card p-4 animate-pulse">
|
||||||
|
<div className="h-5 bg-gray-200 dark:bg-gray-800 rounded w-3/4" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<input
|
||||||
|
className="w-full px-4 py-2.5 rounded-xl bg-gray-100 dark:bg-gray-800 text-sm text-gray-900 dark:text-white placeholder-gray-400 outline-none"
|
||||||
|
placeholder="Поиск по описанию..."
|
||||||
|
value={search}
|
||||||
|
<span class="fstat-no" title="function not covered" > onChange={(e</span>) => <span class="cstat-no" title="statement not covered" >setSearch(e.target.value)}</span>
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[
|
||||||
|
["all", "Все"],
|
||||||
|
["income", "Доходы"],
|
||||||
|
["expense", "Расходы"],
|
||||||
|
].map(([k, l]) => (
|
||||||
|
<button
|
||||||
|
key={k}
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etFilter(k)}</span>
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${
|
||||||
|
filter === k
|
||||||
|
? "bg-primary-500 text-white"
|
||||||
|
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{l}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
||||||
|
<button
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etCatFilter(null)}</span>
|
||||||
|
className={`px-3 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition ${
|
||||||
|
!catFilter
|
||||||
|
? "bg-accent-500 text-white"
|
||||||
|
: <span class="branch-1 cbranch-no" title="branch not covered" >"bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"</span>
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Все
|
||||||
|
</button>
|
||||||
|
{categories.map((c) => (
|
||||||
|
<button
|
||||||
|
key={c.id}
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etCatFilter(c.id)}</span>
|
||||||
|
className={`px-3 py-1 rounded-lg text-xs font-medium whitespace-nowrap transition ${
|
||||||
|
catFilter === c.id
|
||||||
|
? <span class="branch-0 cbranch-no" title="branch not covered" >"bg-accent-500 text-white"</span>
|
||||||
|
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{c.emoji} {c.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{Object.keys(grouped).length === 0 ? (
|
||||||
|
<span class="branch-0 cbranch-no" title="branch not covered" > <div className="card p-12 text-center"></span>
|
||||||
|
<span className="text-4xl block mb-3">🔍</span>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">Ничего не найдено</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
Object.entries(grouped).map(([date, txs]) => (
|
||||||
|
<div key={date}>
|
||||||
|
<p className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase mb-2">
|
||||||
|
{formatDate(date)}
|
||||||
|
</p>
|
||||||
|
<div className="card divide-y divide-gray-100 dark:divide-gray-800">
|
||||||
|
{txs.map((t) => (
|
||||||
|
<div
|
||||||
|
key={t.id}
|
||||||
|
className="px-4 py-3 flex items-center gap-3"
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" >h</span>andleDelete(t.id)}</span>
|
||||||
|
>
|
||||||
|
<span className="text-xl">{t.category_emoji}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||||
|
{t.description || <span class="branch-1 cbranch-no" title="branch not covered" >t.category_name}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{t.category_emoji} {t.category_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-bold ${
|
||||||
|
t.type === "income" ? "text-green-500" : "text-red-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t.type === "income" ? "+" : "-"}
|
||||||
|
{fmt(t.amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
131
coverage/lcov-report/components/finance/index.html
Normal file
131
coverage/lcov-report/components/finance/index.html
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for components/finance</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../../index.html">All files</a> components/finance</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">77.63% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>59/76</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">68.51% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>37/54</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">68.57% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>24/35</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">81.81% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>54/66</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line medium'></div>
|
||||||
|
<div class="pad1">
|
||||||
|
<table class="coverage-summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||||
|
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||||
|
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||||
|
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||||
|
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||||
|
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||||
|
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="file high" data-value="FinanceDashboard.jsx"><a href="FinanceDashboard.jsx.html">FinanceDashboard.jsx</a></td>
|
||||||
|
<td data-value="88.88" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 88%"></div><div class="cover-empty" style="width: 12%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="88.88" class="pct high">88.88%</td>
|
||||||
|
<td data-value="27" class="abs high">24/27</td>
|
||||||
|
<td data-value="83.33" class="pct high">83.33%</td>
|
||||||
|
<td data-value="18" class="abs high">15/18</td>
|
||||||
|
<td data-value="78.57" class="pct medium">78.57%</td>
|
||||||
|
<td data-value="14" class="abs medium">11/14</td>
|
||||||
|
<td data-value="86.95" class="pct high">86.95%</td>
|
||||||
|
<td data-value="23" class="abs high">20/23</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="TransactionList.jsx"><a href="TransactionList.jsx.html">TransactionList.jsx</a></td>
|
||||||
|
<td data-value="71.42" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 71%"></div><div class="cover-empty" style="width: 29%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="71.42" class="pct medium">71.42%</td>
|
||||||
|
<td data-value="49" class="abs medium">35/49</td>
|
||||||
|
<td data-value="61.11" class="pct medium">61.11%</td>
|
||||||
|
<td data-value="36" class="abs medium">22/36</td>
|
||||||
|
<td data-value="61.9" class="pct medium">61.9%</td>
|
||||||
|
<td data-value="21" class="abs medium">13/21</td>
|
||||||
|
<td data-value="79.06" class="pct medium">79.06%</td>
|
||||||
|
<td data-value="43" class="abs medium">34/43</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
191
coverage/lcov-report/components/index.html
Normal file
191
coverage/lcov-report/components/index.html
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for components</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> components</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">62.77% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>285/454</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">49.74% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>195/392</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">33.33% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>55/165</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">63.88% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>283/443</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line medium'></div>
|
||||||
|
<div class="pad1">
|
||||||
|
<table class="coverage-summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||||
|
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||||
|
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||||
|
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||||
|
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||||
|
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||||
|
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="file medium" data-value="CreateHabitModal.jsx"><a href="CreateHabitModal.jsx.html">CreateHabitModal.jsx</a></td>
|
||||||
|
<td data-value="65.82" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 65%"></div><div class="cover-empty" style="width: 35%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="65.82" class="pct medium">65.82%</td>
|
||||||
|
<td data-value="79" class="abs medium">52/79</td>
|
||||||
|
<td data-value="49.15" class="pct low">49.15%</td>
|
||||||
|
<td data-value="59" class="abs low">29/59</td>
|
||||||
|
<td data-value="28.57" class="pct low">28.57%</td>
|
||||||
|
<td data-value="28" class="abs low">8/28</td>
|
||||||
|
<td data-value="65.82" class="pct medium">65.82%</td>
|
||||||
|
<td data-value="79" class="abs medium">52/79</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="CreateTaskModal.jsx"><a href="CreateTaskModal.jsx.html">CreateTaskModal.jsx</a></td>
|
||||||
|
<td data-value="71.05" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 71%"></div><div class="cover-empty" style="width: 29%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="71.05" class="pct medium">71.05%</td>
|
||||||
|
<td data-value="76" class="abs medium">54/76</td>
|
||||||
|
<td data-value="54.09" class="pct medium">54.09%</td>
|
||||||
|
<td data-value="61" class="abs medium">33/61</td>
|
||||||
|
<td data-value="32.14" class="pct low">32.14%</td>
|
||||||
|
<td data-value="28" class="abs low">9/28</td>
|
||||||
|
<td data-value="71.05" class="pct medium">71.05%</td>
|
||||||
|
<td data-value="76" class="abs medium">54/76</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="EditHabitModal.jsx"><a href="EditHabitModal.jsx.html">EditHabitModal.jsx</a></td>
|
||||||
|
<td data-value="52.94" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 52%"></div><div class="cover-empty" style="width: 48%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="52.94" class="pct medium">52.94%</td>
|
||||||
|
<td data-value="153" class="abs medium">81/153</td>
|
||||||
|
<td data-value="39.23" class="pct low">39.23%</td>
|
||||||
|
<td data-value="130" class="abs low">51/130</td>
|
||||||
|
<td data-value="24.07" class="pct low">24.07%</td>
|
||||||
|
<td data-value="54" class="abs low">13/54</td>
|
||||||
|
<td data-value="54" class="pct medium">54%</td>
|
||||||
|
<td data-value="150" class="abs medium">81/150</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="EditTaskModal.jsx"><a href="EditTaskModal.jsx.html">EditTaskModal.jsx</a></td>
|
||||||
|
<td data-value="68.88" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 68%"></div><div class="cover-empty" style="width: 32%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="68.88" class="pct medium">68.88%</td>
|
||||||
|
<td data-value="90" class="abs medium">62/90</td>
|
||||||
|
<td data-value="57.44" class="pct medium">57.44%</td>
|
||||||
|
<td data-value="94" class="abs medium">54/94</td>
|
||||||
|
<td data-value="37.14" class="pct low">37.14%</td>
|
||||||
|
<td data-value="35" class="abs low">13/35</td>
|
||||||
|
<td data-value="69.66" class="pct medium">69.66%</td>
|
||||||
|
<td data-value="89" class="abs medium">62/89</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="LogHabitModal.jsx"><a href="LogHabitModal.jsx.html">LogHabitModal.jsx</a></td>
|
||||||
|
<td data-value="58.33" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 58%"></div><div class="cover-empty" style="width: 42%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="58.33" class="pct medium">58.33%</td>
|
||||||
|
<td data-value="48" class="abs medium">28/48</td>
|
||||||
|
<td data-value="56.52" class="pct medium">56.52%</td>
|
||||||
|
<td data-value="46" class="abs medium">26/46</td>
|
||||||
|
<td data-value="50" class="pct medium">50%</td>
|
||||||
|
<td data-value="16" class="abs medium">8/16</td>
|
||||||
|
<td data-value="64.28" class="pct medium">64.28%</td>
|
||||||
|
<td data-value="42" class="abs medium">27/42</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="Navigation.jsx"><a href="Navigation.jsx.html">Navigation.jsx</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="8" class="abs high">8/8</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="2" class="abs high">2/2</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="4" class="abs high">4/4</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="7" class="abs high">7/7</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
211
coverage/lcov-report/contexts/ThemeContext.jsx.html
Normal file
211
coverage/lcov-report/contexts/ThemeContext.jsx.html
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for contexts/ThemeContext.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">contexts</a> ThemeContext.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">94.73% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>18/19</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">90% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>9/10</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>6/6</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">94.44% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>17/18</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</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-yes">2x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">20x</span>
|
||||||
|
<span class="cline-any cline-yes">15x</span>
|
||||||
|
<span class="cline-any cline-yes">15x</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-yes">20x</span>
|
||||||
|
<span class="cline-any cline-yes">20x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">20x</span>
|
||||||
|
<span class="cline-any cline-yes">12x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">8x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">20x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">20x</span>
|
||||||
|
<span class="cline-any cline-yes">5x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">20x</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">46x</span>
|
||||||
|
<span class="cline-any cline-yes">46x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">41x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { createContext, useContext, useEffect, useState } from "react"
|
||||||
|
|
||||||
|
const ThemeContext = createContext()
|
||||||
|
|
||||||
|
export function ThemeProvider({ children }) {
|
||||||
|
const [theme, setTheme] = useState(() => {
|
||||||
|
<span class="missing-if-branch" title="else path not taken" >E</span>if (typeof window !== "undefined") {
|
||||||
|
return localStorage.getItem("theme") || "dark"
|
||||||
|
}
|
||||||
|
<span class="cstat-no" title="statement not covered" > return "dark"</span>
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = window.document.documentElement
|
||||||
|
|
||||||
|
if (theme === "dark") {
|
||||||
|
root.classList.add("dark")
|
||||||
|
} else {
|
||||||
|
root.classList.remove("dark")
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("theme", theme)
|
||||||
|
}, [theme])
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(prev => prev === "dark" ? "light" : "dark")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const context = useContext(ThemeContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("useTheme must be used within ThemeProvider")
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
116
coverage/lcov-report/contexts/index.html
Normal file
116
coverage/lcov-report/contexts/index.html
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for contexts</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> contexts</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">94.73% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>18/19</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">90% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>9/10</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>6/6</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">94.44% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>17/18</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></div>
|
||||||
|
<div class="pad1">
|
||||||
|
<table class="coverage-summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||||
|
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||||
|
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||||
|
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||||
|
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||||
|
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||||
|
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="file high" data-value="ThemeContext.jsx"><a href="ThemeContext.jsx.html">ThemeContext.jsx</a></td>
|
||||||
|
<td data-value="94.73" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 94%"></div><div class="cover-empty" style="width: 6%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="94.73" class="pct high">94.73%</td>
|
||||||
|
<td data-value="19" class="abs high">18/19</td>
|
||||||
|
<td data-value="90" class="pct high">90%</td>
|
||||||
|
<td data-value="10" class="abs high">9/10</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="6" class="abs high">6/6</td>
|
||||||
|
<td data-value="94.44" class="pct high">94.44%</td>
|
||||||
|
<td data-value="18" class="abs high">17/18</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
BIN
coverage/lcov-report/favicon.png
Normal file
BIN
coverage/lcov-report/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 445 B |
191
coverage/lcov-report/index.html
Normal file
191
coverage/lcov-report/index.html
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for All files</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1>All files</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">56.61% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>929/1641</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">45.66% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>580/1270</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">42.06% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>257/611</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">59.7% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>877/1469</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line medium'></div>
|
||||||
|
<div class="pad1">
|
||||||
|
<table class="coverage-summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||||
|
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||||
|
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||||
|
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||||
|
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||||
|
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||||
|
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="file high" data-value="api"><a href="api/index.html">api</a></td>
|
||||||
|
<td data-value="89.1" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 89%"></div><div class="cover-empty" style="width: 11%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="89.1" class="pct high">89.1%</td>
|
||||||
|
<td data-value="101" class="abs high">90/101</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="12" class="abs high">12/12</td>
|
||||||
|
<td data-value="84.93" class="pct high">84.93%</td>
|
||||||
|
<td data-value="73" class="abs high">62/73</td>
|
||||||
|
<td data-value="90" class="pct high">90%</td>
|
||||||
|
<td data-value="80" class="abs high">72/80</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="components"><a href="components/index.html">components</a></td>
|
||||||
|
<td data-value="62.77" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 62%"></div><div class="cover-empty" style="width: 38%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="62.77" class="pct medium">62.77%</td>
|
||||||
|
<td data-value="454" class="abs medium">285/454</td>
|
||||||
|
<td data-value="49.74" class="pct low">49.74%</td>
|
||||||
|
<td data-value="392" class="abs low">195/392</td>
|
||||||
|
<td data-value="33.33" class="pct low">33.33%</td>
|
||||||
|
<td data-value="165" class="abs low">55/165</td>
|
||||||
|
<td data-value="63.88" class="pct medium">63.88%</td>
|
||||||
|
<td data-value="443" class="abs medium">283/443</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="components/finance"><a href="components/finance/index.html">components/finance</a></td>
|
||||||
|
<td data-value="77.63" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 77%"></div><div class="cover-empty" style="width: 23%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="77.63" class="pct medium">77.63%</td>
|
||||||
|
<td data-value="76" class="abs medium">59/76</td>
|
||||||
|
<td data-value="68.51" class="pct medium">68.51%</td>
|
||||||
|
<td data-value="54" class="abs medium">37/54</td>
|
||||||
|
<td data-value="68.57" class="pct medium">68.57%</td>
|
||||||
|
<td data-value="35" class="abs medium">24/35</td>
|
||||||
|
<td data-value="81.81" class="pct high">81.81%</td>
|
||||||
|
<td data-value="66" class="abs high">54/66</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="contexts"><a href="contexts/index.html">contexts</a></td>
|
||||||
|
<td data-value="94.73" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 94%"></div><div class="cover-empty" style="width: 6%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="94.73" class="pct high">94.73%</td>
|
||||||
|
<td data-value="19" class="abs high">18/19</td>
|
||||||
|
<td data-value="90" class="pct high">90%</td>
|
||||||
|
<td data-value="10" class="abs high">9/10</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="6" class="abs high">6/6</td>
|
||||||
|
<td data-value="94.44" class="pct high">94.44%</td>
|
||||||
|
<td data-value="18" class="abs high">17/18</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file low" data-value="pages"><a href="pages/index.html">pages</a></td>
|
||||||
|
<td data-value="46.79" class="pic low">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 46%"></div><div class="cover-empty" style="width: 54%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="46.79" class="pct low">46.79%</td>
|
||||||
|
<td data-value="966" class="abs low">452/966</td>
|
||||||
|
<td data-value="40.62" class="pct low">40.62%</td>
|
||||||
|
<td data-value="800" class="abs low">325/800</td>
|
||||||
|
<td data-value="32.11" class="pct low">32.11%</td>
|
||||||
|
<td data-value="327" class="abs low">105/327</td>
|
||||||
|
<td data-value="50.95" class="pct medium">50.95%</td>
|
||||||
|
<td data-value="838" class="abs medium">427/838</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="store"><a href="store/index.html">store</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="25" class="abs high">25/25</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="2" class="abs high">2/2</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="5" class="abs high">5/5</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="24" class="abs high">24/24</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
451
coverage/lcov-report/pages/Finance.jsx.html
Normal file
451
coverage/lcov-report/pages/Finance.jsx.html
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/Finance.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Finance.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">44.44% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>16/36</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">75% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>15/20</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">25% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>4/16</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">64% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>16/25</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line low'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</a>
|
||||||
|
<a name='L105'></a><a href='#L105'>105</a>
|
||||||
|
<a name='L106'></a><a href='#L106'>106</a>
|
||||||
|
<a name='L107'></a><a href='#L107'>107</a>
|
||||||
|
<a name='L108'></a><a href='#L108'>108</a>
|
||||||
|
<a name='L109'></a><a href='#L109'>109</a>
|
||||||
|
<a name='L110'></a><a href='#L110'>110</a>
|
||||||
|
<a name='L111'></a><a href='#L111'>111</a>
|
||||||
|
<a name='L112'></a><a href='#L112'>112</a>
|
||||||
|
<a name='L113'></a><a href='#L113'>113</a>
|
||||||
|
<a name='L114'></a><a href='#L114'>114</a>
|
||||||
|
<a name='L115'></a><a href='#L115'>115</a>
|
||||||
|
<a name='L116'></a><a href='#L116'>116</a>
|
||||||
|
<a name='L117'></a><a href='#L117'>117</a>
|
||||||
|
<a name='L118'></a><a href='#L118'>118</a>
|
||||||
|
<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>
|
||||||
|
<a name='L123'></a><a href='#L123'>123</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>
|
||||||
|
<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">1x</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">1x</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">14x</span>
|
||||||
|
<span class="cline-any cline-yes">14x</span>
|
||||||
|
<span class="cline-any cline-yes">14x</span>
|
||||||
|
<span class="cline-any cline-yes">14x</span>
|
||||||
|
<span class="cline-any cline-yes">14x</span>
|
||||||
|
<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-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-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-neutral"> </span>
|
||||||
|
<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-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-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-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-yes">56x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">4x</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">1x</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-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-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></td><td class="text"><pre class="prettyprint lang-js">import { useState } from "react"
|
||||||
|
import Navigation from "../components/Navigation"
|
||||||
|
import FinanceDashboard from "../components/finance/FinanceDashboard"
|
||||||
|
import TransactionList from "../components/finance/TransactionList"
|
||||||
|
import FinanceAnalytics from "../components/finance/FinanceAnalytics"
|
||||||
|
import CategoriesManager from "../components/finance/CategoriesManager"
|
||||||
|
import AddTransactionModal from "../components/finance/AddTransactionModal"
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: "dashboard", label: "Обзор", icon: "📊" },
|
||||||
|
{ key: "transactions", label: "Транзакции", icon: "📋" },
|
||||||
|
{ key: "analytics", label: "Аналитика", icon: "📈" },
|
||||||
|
{ key: "categories", label: "Категории", icon: "🏷️" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
|
||||||
|
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь",
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Finance() {
|
||||||
|
const now = new Date()
|
||||||
|
const [activeTab, setActiveTab] = useState("dashboard")
|
||||||
|
const [showAdd, setShowAdd] = useState(false)
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0)
|
||||||
|
const [month, setMonth] = useState(now.getMonth() + 1)
|
||||||
|
const [year, setYear] = useState(now.getFullYear())
|
||||||
|
|
||||||
|
const <span class="fstat-no" title="function not covered" >refresh = () => <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etRefreshKey((k</span>) => <span class="cstat-no" title="statement not covered" >k + 1)</span></span>
|
||||||
|
|
||||||
|
const <span class="fstat-no" title="function not covered" >prevMonth = () => {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (month === 1) { <span class="cstat-no" title="statement not covered" >setMonth(12); <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etYear(y</span> => <span class="cstat-no" title="statement not covered" >y - 1) </span>}</span></span>
|
||||||
|
else <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >setMonth(m</span> => <span class="cstat-no" title="statement not covered" >m - 1)</span></span>
|
||||||
|
}
|
||||||
|
const <span class="fstat-no" title="function not covered" >nextMonth = () => {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (month === 12) { <span class="cstat-no" title="statement not covered" >setMonth(1); <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etYear(y</span> => <span class="cstat-no" title="statement not covered" >y + 1) </span>}</span></span>
|
||||||
|
else <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >setMonth(m</span> => <span class="cstat-no" title="statement not covered" >m + 1)</span></span>
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentMonth = month === now.getMonth() + 1 && year === now.getFullYear()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24">
|
||||||
|
<header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10">
|
||||||
|
<div className="max-w-lg mx-auto px-4 py-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">
|
||||||
|
💰 Финансы
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etShowAdd(true)}</span>
|
||||||
|
className="w-10 h-10 rounded-xl bg-primary-500 text-white flex items-center justify-center text-xl shadow-lg hover:bg-primary-600 transition"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Month Switcher */}
|
||||||
|
<div className="max-w-lg mx-auto px-4 pb-3">
|
||||||
|
<div className="flex items-center justify-center gap-4">
|
||||||
|
<button
|
||||||
|
onClick={prevMonth}
|
||||||
|
className="w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition text-sm font-bold"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => {</span> <span class="cstat-no" title="statement not covered" >setMonth(now.getMonth() + 1); <span class="cstat-no" title="statement not covered" >s</span>etYear(now.getFullYear()) }}</span>
|
||||||
|
className={"text-sm font-semibold min-w-[140px] text-center " + (isCurrentMonth ? "text-gray-900 dark:text-white" : <span class="branch-1 cbranch-no" title="branch not covered" >"text-primary-600 dark:text-primary-400")}</span>
|
||||||
|
>
|
||||||
|
{MONTH_NAMES[month - 1]} {year}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={nextMonth}
|
||||||
|
className="w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 flex items-center justify-center hover:bg-gray-200 dark:hover:bg-gray-700 transition text-sm font-bold"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-lg mx-auto px-4 pb-3 flex gap-1.5 overflow-x-auto scrollbar-hide">
|
||||||
|
{tabs.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.key}
|
||||||
|
onClick={() => setActiveTab(t.key)}
|
||||||
|
className={`flex-1 min-w-0 py-2 rounded-xl text-xs sm:text-sm font-semibold transition whitespace-nowrap px-2 ${
|
||||||
|
activeTab === t.key
|
||||||
|
? "bg-primary-500 text-white"
|
||||||
|
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t.icon} {t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="max-w-lg mx-auto px-4 py-6">
|
||||||
|
{activeTab === "dashboard" && <FinanceDashboard key={refreshKey + "-" + month + "-" + year} month={month} year={year} />}
|
||||||
|
{activeTab === "transactions" && (
|
||||||
|
<TransactionList key={refreshKey + "-" + month + "-" + year} month={month} year={year} onAdd={() => setShowAdd(true)} />
|
||||||
|
)}
|
||||||
|
{activeTab === "analytics" && <FinanceAnalytics key={refreshKey + "-" + month + "-" + year} month={month} year={year} />}
|
||||||
|
{activeTab === "categories" && <CategoriesManager refreshKey={refreshKey} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showAdd && (
|
||||||
|
<AddTransactionModal
|
||||||
|
<span class="fstat-no" title="function not covered" > onClose={() => <span class="cstat-no" title="statement not covered" >s</span>etShowAdd(false)}</span>
|
||||||
|
<span class="fstat-no" title="function not covered" > onSaved={() => {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > setShowAdd(false)</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > refresh()</span>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Navigation />
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
499
coverage/lcov-report/pages/ForgotPassword.jsx.html
Normal file
499
coverage/lcov-report/pages/ForgotPassword.jsx.html
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/ForgotPassword.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> ForgotPassword.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>17/17</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>8/8</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>3/3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>17/17</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</a>
|
||||||
|
<a name='L105'></a><a href='#L105'>105</a>
|
||||||
|
<a name='L106'></a><a href='#L106'>106</a>
|
||||||
|
<a name='L107'></a><a href='#L107'>107</a>
|
||||||
|
<a name='L108'></a><a href='#L108'>108</a>
|
||||||
|
<a name='L109'></a><a href='#L109'>109</a>
|
||||||
|
<a name='L110'></a><a href='#L110'>110</a>
|
||||||
|
<a name='L111'></a><a href='#L111'>111</a>
|
||||||
|
<a name='L112'></a><a href='#L112'>112</a>
|
||||||
|
<a name='L113'></a><a href='#L113'>113</a>
|
||||||
|
<a name='L114'></a><a href='#L114'>114</a>
|
||||||
|
<a name='L115'></a><a href='#L115'>115</a>
|
||||||
|
<a name='L116'></a><a href='#L116'>116</a>
|
||||||
|
<a name='L117'></a><a href='#L117'>117</a>
|
||||||
|
<a name='L118'></a><a href='#L118'>118</a>
|
||||||
|
<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>
|
||||||
|
<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></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>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">5x</span>
|
||||||
|
<span class="cline-any cline-yes">5x</span>
|
||||||
|
<span class="cline-any cline-yes">5x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">5x</span>
|
||||||
|
<span class="cline-any cline-yes">5x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">2x</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>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</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-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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">19x</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-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>
|
||||||
|
<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-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></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Mail, ArrowLeft, Zap, CheckCircle } from 'lucide-react'
|
||||||
|
import api from '../api/client'
|
||||||
|
|
||||||
|
export default function ForgotPassword() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [sent, setSent] = useState(false)
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post('/auth/forgot-password', { email })
|
||||||
|
setSent(true)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || 'Ошибка отправки')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="w-full max-w-md"
|
||||||
|
>
|
||||||
|
<div className="card p-10 text-center">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 200 }}
|
||||||
|
className="w-20 h-20 rounded-3xl bg-green-100 flex items-center justify-center mx-auto mb-6"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-2xl font-display font-bold text-gray-900 mb-2">
|
||||||
|
Письмо отправлено! 📬
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mb-6">
|
||||||
|
Если аккаунт с email <strong>{email}</strong> существует, мы отправили ссылку для сброса пароля.
|
||||||
|
</p>
|
||||||
|
<Link to="/login" className="btn btn-primary">
|
||||||
|
Вернуться ко входу
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="w-full max-w-md"
|
||||||
|
>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', delay: 0.1 }}
|
||||||
|
className="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-500 to-primary-700 mb-6 shadow-xl shadow-primary-500/30"
|
||||||
|
>
|
||||||
|
<Mail className="w-10 h-10 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-3xl font-display font-bold text-gray-900">
|
||||||
|
Забыли пароль?
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mt-2">
|
||||||
|
Введи email и мы отправим ссылку для сброса
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card p-8">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
className="p-4 rounded-2xl bg-red-50 text-red-600 text-sm font-medium"
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="input"
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="btn btn-primary w-full text-lg"
|
||||||
|
>
|
||||||
|
{loading ? 'Отправляем...' : 'Отправить ссылку'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<Link
|
||||||
|
to="/login"
|
||||||
|
className="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium text-sm"
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} />
|
||||||
|
Вернуться ко входу
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-6 text-gray-400">
|
||||||
|
<Zap size={16} />
|
||||||
|
<span className="text-sm font-medium">Pulse</span>
|
||||||
|
</div>
|
||||||
|
</motion.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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
745
coverage/lcov-report/pages/Habits.jsx.html
Normal file
745
coverage/lcov-report/pages/Habits.jsx.html
Normal file
@@ -0,0 +1,745 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/Habits.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Habits.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">64.91% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>37/57</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">65.9% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>29/44</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">53.57% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>15/28</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">65.21% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>30/46</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</a>
|
||||||
|
<a name='L105'></a><a href='#L105'>105</a>
|
||||||
|
<a name='L106'></a><a href='#L106'>106</a>
|
||||||
|
<a name='L107'></a><a href='#L107'>107</a>
|
||||||
|
<a name='L108'></a><a href='#L108'>108</a>
|
||||||
|
<a name='L109'></a><a href='#L109'>109</a>
|
||||||
|
<a name='L110'></a><a href='#L110'>110</a>
|
||||||
|
<a name='L111'></a><a href='#L111'>111</a>
|
||||||
|
<a name='L112'></a><a href='#L112'>112</a>
|
||||||
|
<a name='L113'></a><a href='#L113'>113</a>
|
||||||
|
<a name='L114'></a><a href='#L114'>114</a>
|
||||||
|
<a name='L115'></a><a href='#L115'>115</a>
|
||||||
|
<a name='L116'></a><a href='#L116'>116</a>
|
||||||
|
<a name='L117'></a><a href='#L117'>117</a>
|
||||||
|
<a name='L118'></a><a href='#L118'>118</a>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<a name='L182'></a><a href='#L182'>182</a>
|
||||||
|
<a name='L183'></a><a href='#L183'>183</a>
|
||||||
|
<a name='L184'></a><a href='#L184'>184</a>
|
||||||
|
<a name='L185'></a><a href='#L185'>185</a>
|
||||||
|
<a name='L186'></a><a href='#L186'>186</a>
|
||||||
|
<a name='L187'></a><a href='#L187'>187</a>
|
||||||
|
<a name='L188'></a><a href='#L188'>188</a>
|
||||||
|
<a name='L189'></a><a href='#L189'>189</a>
|
||||||
|
<a name='L190'></a><a href='#L190'>190</a>
|
||||||
|
<a name='L191'></a><a href='#L191'>191</a>
|
||||||
|
<a name='L192'></a><a href='#L192'>192</a>
|
||||||
|
<a name='L193'></a><a href='#L193'>193</a>
|
||||||
|
<a name='L194'></a><a href='#L194'>194</a>
|
||||||
|
<a name='L195'></a><a href='#L195'>195</a>
|
||||||
|
<a name='L196'></a><a href='#L196'>196</a>
|
||||||
|
<a name='L197'></a><a href='#L197'>197</a>
|
||||||
|
<a name='L198'></a><a href='#L198'>198</a>
|
||||||
|
<a name='L199'></a><a href='#L199'>199</a>
|
||||||
|
<a name='L200'></a><a href='#L200'>200</a>
|
||||||
|
<a name='L201'></a><a href='#L201'>201</a>
|
||||||
|
<a name='L202'></a><a href='#L202'>202</a>
|
||||||
|
<a name='L203'></a><a href='#L203'>203</a>
|
||||||
|
<a name='L204'></a><a href='#L204'>204</a>
|
||||||
|
<a name='L205'></a><a href='#L205'>205</a>
|
||||||
|
<a name='L206'></a><a href='#L206'>206</a>
|
||||||
|
<a name='L207'></a><a href='#L207'>207</a>
|
||||||
|
<a name='L208'></a><a href='#L208'>208</a>
|
||||||
|
<a name='L209'></a><a href='#L209'>209</a>
|
||||||
|
<a name='L210'></a><a href='#L210'>210</a>
|
||||||
|
<a name='L211'></a><a href='#L211'>211</a>
|
||||||
|
<a name='L212'></a><a href='#L212'>212</a>
|
||||||
|
<a name='L213'></a><a href='#L213'>213</a>
|
||||||
|
<a name='L214'></a><a href='#L214'>214</a>
|
||||||
|
<a name='L215'></a><a href='#L215'>215</a>
|
||||||
|
<a name='L216'></a><a href='#L216'>216</a>
|
||||||
|
<a name='L217'></a><a href='#L217'>217</a>
|
||||||
|
<a name='L218'></a><a href='#L218'>218</a>
|
||||||
|
<a name='L219'></a><a href='#L219'>219</a>
|
||||||
|
<a name='L220'></a><a href='#L220'>220</a>
|
||||||
|
<a name='L221'></a><a href='#L221'>221</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>
|
||||||
|
<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">11x</span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">10x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</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-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">9x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">15x</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-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</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">1x</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">18x</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-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-yes">6x</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-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-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-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-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-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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">6x</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-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></td><td class="text"><pre class="prettyprint lang-js">import { useState, useEffect } from 'react'
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { Plus, Settings, Flame, Calendar, ChevronRight, Archive, ArchiveRestore } from 'lucide-react'
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import { ru } from 'date-fns/locale'
|
||||||
|
import { habitsApi } from '../api/habits'
|
||||||
|
import CreateHabitModal from '../components/CreateHabitModal'
|
||||||
|
import EditHabitModal from '../components/EditHabitModal'
|
||||||
|
import Navigation from '../components/Navigation'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export default function Habits({ embedded = false }) {
|
||||||
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||||
|
const [editingHabit, setEditingHabit] = useState(null)
|
||||||
|
const [showArchived, setShowArchived] = useState(false)
|
||||||
|
const [habitStats, setHabitStats] = useState({})
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const { data: habits = [], isLoading } = useQuery({
|
||||||
|
queryKey: ['habits', showArchived],
|
||||||
|
queryFn: () => habitsApi.list().then(h => showArchived ? <span class="branch-0 cbranch-no" title="branch not covered" >h : h</span>.filter(x => !x.is_archived)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: archivedHabits = [] } = useQuery({
|
||||||
|
queryKey: ['habits-archived'],
|
||||||
|
<span class="fstat-no" title="function not covered" > queryFn: () => <span class="cstat-no" title="statement not covered" >h</span>abitsApi.list().<span class="fstat-no" title="function not covered" >then(h</span> => <span class="cstat-no" title="statement not covered" >h.<span class="fstat-no" title="function not covered" >filter(x</span> => <span class="cstat-no" title="statement not covered" >x.is_archived))</span></span>,</span>
|
||||||
|
enabled: showArchived,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (habits.length > 0) loadStats()
|
||||||
|
}, [habits])
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
const statsMap = {}
|
||||||
|
await Promise.all(habits.map(async (habit) => {
|
||||||
|
try {
|
||||||
|
const stats = await habitsApi.getHabitStats(habit.id)
|
||||||
|
statsMap[habit.id] = stats
|
||||||
|
} catch (e) {}
|
||||||
|
}))
|
||||||
|
setHabitStats(statsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
const archiveMutation = useMutation({
|
||||||
|
<span class="fstat-no" title="function not covered" > mutationFn: ({</span> id, archived }) => <span class="cstat-no" title="statement not covered" >habitsApi.update(id, { is_archived: archived }),</span>
|
||||||
|
<span class="fstat-no" title="function not covered" > onSuccess: () => {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['habits'] })</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['habits-archived'] })</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['stats'] })</span>
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const getFrequencyLabel = (habit) => {
|
||||||
|
if (habit.frequency === 'daily') return 'Ежедневно'
|
||||||
|
<span class="missing-if-branch" title="else path not taken" >E</span>if (habit.frequency === 'weekly' && habit.target_days) {
|
||||||
|
const days = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']
|
||||||
|
return habit.target_days.map(d => days[d - 1]).join(', ')
|
||||||
|
}
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (habit.frequency === 'interval') <span class="cstat-no" title="statement not covered" >return `Каждые ${habit.target_count} дн.`</span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (habit.frequency === 'custom') <span class="cstat-no" title="statement not covered" >return `Каждые ${habit.target_count} дн.`</span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > return habit.frequency</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeHabits = habits.filter(h => !h.is_archived)
|
||||||
|
const archivedList = habits.filter(h => h.is_archived)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={embedded ? "" : "min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24 transition-colors duration-300"}>
|
||||||
|
{!embedded && <header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10">
|
||||||
|
<div className="max-w-lg mx-auto px-4 py-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">Мои привычки</h1>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">{activeHabits.length} активных</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setShowCreateModal(true)} className="btn btn-primary flex items-center gap-2">
|
||||||
|
<Plus size={18} />
|
||||||
|
Новая
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>}
|
||||||
|
|
||||||
|
<main className="max-w-lg mx-auto px-4 py-6 space-y-6">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="card p-5 animate-pulse">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-14 h-14 rounded-2xl bg-gray-200 dark:bg-gray-700" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="h-5 bg-gray-200 dark:bg-gray-700 rounded-lg w-1/2 mb-2" />
|
||||||
|
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded-lg w-1/3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : activeHabits.length === 0 && !showArchived ? (
|
||||||
|
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="card p-10 text-center">
|
||||||
|
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-100 to-accent-100 dark:from-primary-900/30 dark:to-accent-900/30 flex items-center justify-center mx-auto mb-5">
|
||||||
|
<Plus className="w-10 h-10 text-primary-600 dark:text-primary-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-2">Нет привычек</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mb-6">Создай свою первую привычку!</p>
|
||||||
|
<button <span class="fstat-no" title="function not covered" >onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etShowCreateModal(true)}</span> className="btn btn-primary">
|
||||||
|
<Plus size={20} className="mr-2" />
|
||||||
|
Создать привычку
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<AnimatePresence>
|
||||||
|
{activeHabits.map((habit, index) => (
|
||||||
|
<HabitListItem
|
||||||
|
key={habit.id}
|
||||||
|
habit={habit}
|
||||||
|
index={index}
|
||||||
|
stats={habitStats[habit.id]}
|
||||||
|
frequencyLabel={getFrequencyLabel(habit)}
|
||||||
|
<span class="fstat-no" title="function not covered" > onEdit={() => <span class="cstat-no" title="statement not covered" >s</span>etEditingHabit(habit)}</span>
|
||||||
|
<span class="fstat-no" title="function not covered" > onArchive={() => <span class="cstat-no" title="statement not covered" >a</span>rchiveMutation.mutate({ id: habit.id, archived: true })}</span>
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{archivedList.length > 0 && (
|
||||||
|
<span class="branch-1 cbranch-no" title="branch not covered" > <div className="mt-8"></span>
|
||||||
|
<button <span class="fstat-no" title="function not covered" >onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etShowArchived(!showArchived)}</span> className="flex items-center gap-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 mb-4">
|
||||||
|
<Archive size={18} />
|
||||||
|
<span className="font-medium">Архив ({archivedList.length})</span>
|
||||||
|
<ChevronRight size={18} className={clsx('transition-transform', showArchived && 'rotate-90')} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{showArchived && (
|
||||||
|
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} exit={{ opacity: 0, height: 0 }} className="space-y-3">
|
||||||
|
{archivedList.<span class="fstat-no" title="function not covered" >map((h</span>abit, index) => (
|
||||||
|
<span class="cstat-no" title="statement not covered" > <motion.div</span>
|
||||||
|
key={habit.id}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="card p-4 opacity-60"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-12 h-12 rounded-xl flex items-center justify-center text-xl" style={{ backgroundColor: habit.color + '20' }}>
|
||||||
|
{habit.icon || '✨'}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-gray-600 dark:text-gray-400 truncate">{habit.name}</h3>
|
||||||
|
<p className="text-sm text-gray-400 dark:text-gray-500">{getFrequencyLabel(habit)}</p>
|
||||||
|
</div>
|
||||||
|
<button <span class="fstat-no" title="function not covered" >onClick={() => <span class="cstat-no" title="statement not covered" >a</span>rchiveMutation.mutate({ id: habit.id, archived: false })}</span> className="p-2 text-gray-400 hover:text-green-500 hover:bg-green-50 dark:hover:bg-green-900/20 rounded-xl transition-all" title="Восстановить">
|
||||||
|
<ArchiveRestore size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{!embedded && <Navigation />}
|
||||||
|
<CreateHabitModal open={showCreateModal} <span class="fstat-no" title="function not covered" >onClose={() => <span class="cstat-no" title="statement not covered" >s</span>etShowCreateModal(false)} /></span>
|
||||||
|
<EditHabitModal open={!!editingHabit} <span class="fstat-no" title="function not covered" >onClose={() => <span class="cstat-no" title="statement not covered" >s</span>etEditingHabit(null)}</span> habit={editingHabit} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function HabitListItem({ habit, index, stats, frequencyLabel, onEdit, onArchive }) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -100 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
onClick={onEdit}
|
||||||
|
className="card p-4 cursor-pointer hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-14 h-14 rounded-2xl flex items-center justify-center text-2xl flex-shrink-0" style={{ backgroundColor: habit.color + '15' }}>
|
||||||
|
{habit.icon || <span class="branch-1 cbranch-no" title="branch not covered" >'✨'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white truncate">{habit.name}</h3>
|
||||||
|
<div className="flex items-center gap-3 mt-1">
|
||||||
|
<span className="text-xs font-medium px-2 py-0.5 rounded-full" style={{ backgroundColor: habit.color + '15', color: habit.color }}>
|
||||||
|
{frequencyLabel}
|
||||||
|
</span>
|
||||||
|
{stats && stats.current_streak > 0 && (
|
||||||
|
<span class="branch-2 cbranch-no" title="branch not covered" > <span className="text-xs text-orange-500 flex items-center gap-1"></span>
|
||||||
|
<Flame size={14} />
|
||||||
|
{stats.current_streak} дн.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{stats && (
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="text-sm font-semibold text-gray-900 dark:text-white">{stats.this_month}</p>
|
||||||
|
<p className="text-xs text-gray-400 dark:text-gray-500">в месяц</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ChevronRight size={20} className="text-gray-300 dark:text-gray-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
1816
coverage/lcov-report/pages/Home.jsx.html
Normal file
1816
coverage/lcov-report/pages/Home.jsx.html
Normal file
File diff suppressed because it is too large
Load Diff
373
coverage/lcov-report/pages/Login.jsx.html
Normal file
373
coverage/lcov-report/pages/Login.jsx.html
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/Login.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Login.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>21/21</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>10/10</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>6/6</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>20/20</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</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>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</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">3x</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">3x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</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-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></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
|
||||||
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Eye, EyeOff, Zap } from 'lucide-react'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const login = useAuthStore(s => s.login)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await login(email, password)
|
||||||
|
navigate('/')
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || 'Ошибка входа')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50 dark:bg-gray-950 transition-colors duration-300">
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} className="w-full max-w-md">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<motion.div initial={{ scale: 0, rotate: -180 }} animate={{ scale: 1, rotate: 0 }} transition={{ type: 'spring', delay: 0.1, stiffness: 200 }} className="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-500 to-primary-700 mb-6 shadow-xl shadow-primary-500/30">
|
||||||
|
<Zap className="w-10 h-10 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
<motion.h1 initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.2 }} className="text-3xl font-display font-bold text-gray-900 dark:text-white">
|
||||||
|
С возвращением!
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.3 }} className="text-gray-500 dark:text-gray-400 mt-2">
|
||||||
|
Войди, чтобы продолжить
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} className="card p-8">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{error && (
|
||||||
|
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} className="p-4 rounded-2xl bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm font-medium">
|
||||||
|
{error}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Email</label>
|
||||||
|
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="input" placeholder="your@email.com" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Пароль</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input type={showPassword ? 'text' : 'password'} value={password} onChange={(e) => setPassword(e.target.value)} className="input pr-12" placeholder="••••••••" required />
|
||||||
|
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||||
|
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Link to="/forgot-password" className="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 font-medium">Забыли пароль?</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={loading} className="btn btn-primary w-full text-lg">
|
||||||
|
{loading ? (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||||
|
</svg>
|
||||||
|
Входим...
|
||||||
|
</span>
|
||||||
|
) : 'Войти'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-8 pt-6 border-t border-gray-100 dark:border-gray-800 text-center">
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
Нет аккаунта?{' '}<Link to="/register" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 font-semibold">Зарегистрируйся</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
337
coverage/lcov-report/pages/Register.jsx.html
Normal file
337
coverage/lcov-report/pages/Register.jsx.html
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/Register.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Register.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>23/23</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>10/10</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>7/7</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>22/22</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</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>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">22x</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">3x</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">3x</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">3x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
|
||||||
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Eye, EyeOff, Sparkles } from 'lucide-react'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
export default function Register() {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [username, setUsername] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const register = useAuthStore(s => s.register)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await register(email, username, password)
|
||||||
|
navigate('/')
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || 'Ошибка регистрации')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 bg-gradient-to-br from-primary-50 via-white to-accent-50 dark:from-gray-950 dark:via-gray-900 dark:to-gray-950 transition-colors duration-300">
|
||||||
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="w-full max-w-md">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: 'spring', delay: 0.1 }} className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-primary-500 to-accent-500 mb-4">
|
||||||
|
<Sparkles className="w-8 h-8 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Создай аккаунт</h1>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mt-1">Начни отслеживать свои привычки</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card p-6">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<motion.div initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: 'auto' }} className="p-3 rounded-xl bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm">
|
||||||
|
{error}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Как тебя зовут?</label>
|
||||||
|
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} className="input" placeholder="Имя" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Email</label>
|
||||||
|
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="input" placeholder="your@email.com" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">Пароль</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input type={showPassword ? 'text' : 'password'} value={password} onChange={(e) => setPassword(e.target.value)} className="input pr-12" placeholder="Минимум 8 символов" minLength={8} required />
|
||||||
|
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||||
|
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={loading} className="btn btn-primary w-full">
|
||||||
|
{loading ? 'Создаём...' : 'Создать аккаунт'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p className="text-center text-sm text-gray-500 dark:text-gray-400 mt-6">
|
||||||
|
Уже есть аккаунт?{' '}<Link to="/login" className="text-primary-600 dark:text-primary-400 hover:text-primary-700 font-medium">Войти</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
505
coverage/lcov-report/pages/ResetPassword.jsx.html
Normal file
505
coverage/lcov-report/pages/ResetPassword.jsx.html
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/ResetPassword.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> ResetPassword.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">96.29% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>26/27</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">92.85% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>13/14</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">80% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>4/5</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>26/26</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</a>
|
||||||
|
<a name='L105'></a><a href='#L105'>105</a>
|
||||||
|
<a name='L106'></a><a href='#L106'>106</a>
|
||||||
|
<a name='L107'></a><a href='#L107'>107</a>
|
||||||
|
<a name='L108'></a><a href='#L108'>108</a>
|
||||||
|
<a name='L109'></a><a href='#L109'>109</a>
|
||||||
|
<a name='L110'></a><a href='#L110'>110</a>
|
||||||
|
<a name='L111'></a><a href='#L111'>111</a>
|
||||||
|
<a name='L112'></a><a href='#L112'>112</a>
|
||||||
|
<a name='L113'></a><a href='#L113'>113</a>
|
||||||
|
<a name='L114'></a><a href='#L114'>114</a>
|
||||||
|
<a name='L115'></a><a href='#L115'>115</a>
|
||||||
|
<a name='L116'></a><a href='#L116'>116</a>
|
||||||
|
<a name='L117'></a><a href='#L117'>117</a>
|
||||||
|
<a name='L118'></a><a href='#L118'>118</a>
|
||||||
|
<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>
|
||||||
|
<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></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>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">18x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</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">16x</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-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">4x</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">1x</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-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
|
||||||
|
import { useSearchParams, Link, useNavigate } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Eye, EyeOff, Zap, CheckCircle } from 'lucide-react'
|
||||||
|
import api from '../api/client'
|
||||||
|
|
||||||
|
export default function ResetPassword() {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [showPassword, setShowPassword] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [success, setSuccess] = useState(false)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const token = searchParams.get('token')
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!token) {
|
||||||
|
setError('Токен не найден')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post('/auth/reset-password', { token, new_password: password })
|
||||||
|
setSuccess(true)
|
||||||
|
<span class="fstat-no" title="function not covered" > setTimeout(() => <span class="cstat-no" title="statement not covered" >n</span>avigate('/login'),</span> 2000)
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.response?.data?.error || <span class="branch-1 cbranch-no" title="branch not covered" >'Ошибка сброса пароля')</span>
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
className="card p-10 text-center max-w-md w-full"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 200 }}
|
||||||
|
className="w-20 h-20 rounded-3xl bg-green-100 flex items-center justify-center mx-auto mb-6"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-2xl font-display font-bold text-gray-900 mb-2">
|
||||||
|
Пароль изменён! 🎉
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500">Перенаправляем на страницу входа...</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="w-full max-w-md"
|
||||||
|
>
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', delay: 0.1 }}
|
||||||
|
className="inline-flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-500 to-primary-700 mb-6 shadow-xl shadow-primary-500/30"
|
||||||
|
>
|
||||||
|
<Zap className="w-10 h-10 text-white" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-3xl font-display font-bold text-gray-900">
|
||||||
|
Новый пароль
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mt-2">Придумай новый надёжный пароль</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card p-8">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
className="p-4 rounded-2xl bg-red-50 text-red-600 text-sm font-medium"
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-semibold text-gray-700 mb-2">
|
||||||
|
Новый пароль
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="input pr-12"
|
||||||
|
placeholder="Минимум 8 символов"
|
||||||
|
minLength={8}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPassword(!showPassword)}
|
||||||
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="btn btn-primary w-full text-lg"
|
||||||
|
>
|
||||||
|
{loading ? 'Сохраняем...' : 'Сохранить пароль'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<Link to="/login" className="text-primary-600 hover:text-primary-700 font-medium text-sm">
|
||||||
|
Вернуться ко входу
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
4273
coverage/lcov-report/pages/Savings.jsx.html
Normal file
4273
coverage/lcov-report/pages/Savings.jsx.html
Normal file
File diff suppressed because it is too large
Load Diff
1123
coverage/lcov-report/pages/Settings.jsx.html
Normal file
1123
coverage/lcov-report/pages/Settings.jsx.html
Normal file
File diff suppressed because it is too large
Load Diff
2170
coverage/lcov-report/pages/Stats.jsx.html
Normal file
2170
coverage/lcov-report/pages/Stats.jsx.html
Normal file
File diff suppressed because it is too large
Load Diff
823
coverage/lcov-report/pages/Tasks.jsx.html
Normal file
823
coverage/lcov-report/pages/Tasks.jsx.html
Normal file
@@ -0,0 +1,823 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/Tasks.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Tasks.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">50% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>28/56</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">58.66% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>44/75</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">31.81% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>7/22</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">60% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>27/45</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</a>
|
||||||
|
<a name='L105'></a><a href='#L105'>105</a>
|
||||||
|
<a name='L106'></a><a href='#L106'>106</a>
|
||||||
|
<a name='L107'></a><a href='#L107'>107</a>
|
||||||
|
<a name='L108'></a><a href='#L108'>108</a>
|
||||||
|
<a name='L109'></a><a href='#L109'>109</a>
|
||||||
|
<a name='L110'></a><a href='#L110'>110</a>
|
||||||
|
<a name='L111'></a><a href='#L111'>111</a>
|
||||||
|
<a name='L112'></a><a href='#L112'>112</a>
|
||||||
|
<a name='L113'></a><a href='#L113'>113</a>
|
||||||
|
<a name='L114'></a><a href='#L114'>114</a>
|
||||||
|
<a name='L115'></a><a href='#L115'>115</a>
|
||||||
|
<a name='L116'></a><a href='#L116'>116</a>
|
||||||
|
<a name='L117'></a><a href='#L117'>117</a>
|
||||||
|
<a name='L118'></a><a href='#L118'>118</a>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<a name='L182'></a><a href='#L182'>182</a>
|
||||||
|
<a name='L183'></a><a href='#L183'>183</a>
|
||||||
|
<a name='L184'></a><a href='#L184'>184</a>
|
||||||
|
<a name='L185'></a><a href='#L185'>185</a>
|
||||||
|
<a name='L186'></a><a href='#L186'>186</a>
|
||||||
|
<a name='L187'></a><a href='#L187'>187</a>
|
||||||
|
<a name='L188'></a><a href='#L188'>188</a>
|
||||||
|
<a name='L189'></a><a href='#L189'>189</a>
|
||||||
|
<a name='L190'></a><a href='#L190'>190</a>
|
||||||
|
<a name='L191'></a><a href='#L191'>191</a>
|
||||||
|
<a name='L192'></a><a href='#L192'>192</a>
|
||||||
|
<a name='L193'></a><a href='#L193'>193</a>
|
||||||
|
<a name='L194'></a><a href='#L194'>194</a>
|
||||||
|
<a name='L195'></a><a href='#L195'>195</a>
|
||||||
|
<a name='L196'></a><a href='#L196'>196</a>
|
||||||
|
<a name='L197'></a><a href='#L197'>197</a>
|
||||||
|
<a name='L198'></a><a href='#L198'>198</a>
|
||||||
|
<a name='L199'></a><a href='#L199'>199</a>
|
||||||
|
<a name='L200'></a><a href='#L200'>200</a>
|
||||||
|
<a name='L201'></a><a href='#L201'>201</a>
|
||||||
|
<a name='L202'></a><a href='#L202'>202</a>
|
||||||
|
<a name='L203'></a><a href='#L203'>203</a>
|
||||||
|
<a name='L204'></a><a href='#L204'>204</a>
|
||||||
|
<a name='L205'></a><a href='#L205'>205</a>
|
||||||
|
<a name='L206'></a><a href='#L206'>206</a>
|
||||||
|
<a name='L207'></a><a href='#L207'>207</a>
|
||||||
|
<a name='L208'></a><a href='#L208'>208</a>
|
||||||
|
<a name='L209'></a><a href='#L209'>209</a>
|
||||||
|
<a name='L210'></a><a href='#L210'>210</a>
|
||||||
|
<a name='L211'></a><a href='#L211'>211</a>
|
||||||
|
<a name='L212'></a><a href='#L212'>212</a>
|
||||||
|
<a name='L213'></a><a href='#L213'>213</a>
|
||||||
|
<a name='L214'></a><a href='#L214'>214</a>
|
||||||
|
<a name='L215'></a><a href='#L215'>215</a>
|
||||||
|
<a name='L216'></a><a href='#L216'>216</a>
|
||||||
|
<a name='L217'></a><a href='#L217'>217</a>
|
||||||
|
<a name='L218'></a><a href='#L218'>218</a>
|
||||||
|
<a name='L219'></a><a href='#L219'>219</a>
|
||||||
|
<a name='L220'></a><a href='#L220'>220</a>
|
||||||
|
<a name='L221'></a><a href='#L221'>221</a>
|
||||||
|
<a name='L222'></a><a href='#L222'>222</a>
|
||||||
|
<a name='L223'></a><a href='#L223'>223</a>
|
||||||
|
<a name='L224'></a><a href='#L224'>224</a>
|
||||||
|
<a name='L225'></a><a href='#L225'>225</a>
|
||||||
|
<a name='L226'></a><a href='#L226'>226</a>
|
||||||
|
<a name='L227'></a><a href='#L227'>227</a>
|
||||||
|
<a name='L228'></a><a href='#L228'>228</a>
|
||||||
|
<a name='L229'></a><a href='#L229'>229</a>
|
||||||
|
<a name='L230'></a><a href='#L230'>230</a>
|
||||||
|
<a name='L231'></a><a href='#L231'>231</a>
|
||||||
|
<a name='L232'></a><a href='#L232'>232</a>
|
||||||
|
<a name='L233'></a><a href='#L233'>233</a>
|
||||||
|
<a name='L234'></a><a href='#L234'>234</a>
|
||||||
|
<a name='L235'></a><a href='#L235'>235</a>
|
||||||
|
<a name='L236'></a><a href='#L236'>236</a>
|
||||||
|
<a name='L237'></a><a href='#L237'>237</a>
|
||||||
|
<a name='L238'></a><a href='#L238'>238</a>
|
||||||
|
<a name='L239'></a><a href='#L239'>239</a>
|
||||||
|
<a name='L240'></a><a href='#L240'>240</a>
|
||||||
|
<a name='L241'></a><a href='#L241'>241</a>
|
||||||
|
<a name='L242'></a><a href='#L242'>242</a>
|
||||||
|
<a name='L243'></a><a href='#L243'>243</a>
|
||||||
|
<a name='L244'></a><a href='#L244'>244</a>
|
||||||
|
<a name='L245'></a><a href='#L245'>245</a>
|
||||||
|
<a name='L246'></a><a href='#L246'>246</a>
|
||||||
|
<a name='L247'></a><a href='#L247'>247</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>
|
||||||
|
<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">1x</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">1x</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">4x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</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">10x</span>
|
||||||
|
<span class="cline-any cline-yes">10x</span>
|
||||||
|
<span class="cline-any cline-yes">10x</span>
|
||||||
|
<span class="cline-any cline-yes">10x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">10x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">7x</span>
|
||||||
|
<span class="cline-any cline-yes">7x</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">10x</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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">10x</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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">10x</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-yes">10x</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-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-yes">27x</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-yes">21x</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-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-yes">4x</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-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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-yes">4x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">4x</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-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">4x</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-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-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-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></td><td class="text"><pre class="prettyprint lang-js">import { useState } from 'react'
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
import { Plus, Check, Circle, Calendar, AlertTriangle, Undo2, Edit2, Repeat } from 'lucide-react'
|
||||||
|
import { format, parseISO, isToday, isTomorrow, isPast } from 'date-fns'
|
||||||
|
import { ru } from 'date-fns/locale'
|
||||||
|
import { tasksApi } from '../api/tasks'
|
||||||
|
import Navigation from '../components/Navigation'
|
||||||
|
import CreateTaskModal from '../components/CreateTaskModal'
|
||||||
|
import EditTaskModal from '../components/EditTaskModal'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
const PRIORITY_LABELS = {
|
||||||
|
0: null,
|
||||||
|
1: { label: 'Низкий', class: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' },
|
||||||
|
2: { label: 'Средний', class: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' },
|
||||||
|
3: { label: 'Высокий', class: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const RECURRENCE_LABELS = {
|
||||||
|
daily: 'Ежедневно',
|
||||||
|
weekly: 'Еженедельно',
|
||||||
|
monthly: 'Ежемесячно',
|
||||||
|
custom: 'Повтор',
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDueDate(dateStr) {
|
||||||
|
if (!dateStr) return null
|
||||||
|
const date = parseISO(dateStr)
|
||||||
|
<span class="missing-if-branch" title="if path not taken" >I</span>if (isToday(date)) <span class="cstat-no" title="statement not covered" >return 'Сегодня'</span>
|
||||||
|
<span class="missing-if-branch" title="if path not taken" >I</span>if (isTomorrow(date)) <span class="cstat-no" title="statement not covered" >return 'Завтра'</span>
|
||||||
|
return format(date, 'd MMM', { locale: ru })
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Tasks({ embedded = false }) {
|
||||||
|
const [showCreate, setShowCreate] = useState(false)
|
||||||
|
const [editingTask, setEditingTask] = useState(null)
|
||||||
|
const [filter, setFilter] = useState('active')
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const { data: tasks = [], isLoading } = useQuery({
|
||||||
|
queryKey: ['tasks', filter],
|
||||||
|
queryFn: () => {
|
||||||
|
<span class="missing-if-branch" title="if path not taken" >I</span>if (filter === 'all') <span class="cstat-no" title="statement not covered" >return tasksApi.list()</span>
|
||||||
|
return tasksApi.list(filter === 'completed')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const completeMutation = useMutation({
|
||||||
|
<span class="fstat-no" title="function not covered" > mutationFn: (i</span>d) => <span class="cstat-no" title="statement not covered" >tasksApi.complete(id),</span>
|
||||||
|
<span class="fstat-no" title="function not covered" > onSuccess: () => {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['tasks'] })</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['tasks-today'] })</span>
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const uncompleteMutation = useMutation({
|
||||||
|
<span class="fstat-no" title="function not covered" > mutationFn: (i</span>d) => <span class="cstat-no" title="statement not covered" >tasksApi.uncomplete(id),</span>
|
||||||
|
<span class="fstat-no" title="function not covered" > onSuccess: () => {</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['tasks'] })</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > queryClient.invalidateQueries({ queryKey: ['tasks-today'] })</span>
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const <span class="fstat-no" title="function not covered" >handleToggle = (t</span>ask) => {
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (task.completed) <span class="cstat-no" title="statement not covered" >uncompleteMutation.mutate(task.id)</span></span>
|
||||||
|
else <span class="cstat-no" title="statement not covered" >completeMutation.mutate(task.id)</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={embedded ? "" : "min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24 transition-colors duration-300"}>
|
||||||
|
{!embedded && <header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10">
|
||||||
|
<div className="max-w-lg mx-auto px-4 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">Задачи</h1>
|
||||||
|
<button <span class="fstat-no" title="function not covered" >onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etShowCreate(true)}</span> className="p-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-colors shadow-lg shadow-primary-500/30">
|
||||||
|
<Plus size={22} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
{[
|
||||||
|
{ key: 'active', label: 'Активные' },
|
||||||
|
{ key: 'completed', label: 'Выполненные' },
|
||||||
|
{ key: 'all', label: 'Все' },
|
||||||
|
].map(({ key, label }) => (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
<span class="fstat-no" title="function not covered" > onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etFilter(key)}</span>
|
||||||
|
className={clsx(
|
||||||
|
'px-4 py-2 rounded-xl text-sm font-medium transition-all',
|
||||||
|
filter === key
|
||||||
|
? 'bg-primary-500 text-white shadow-lg shadow-primary-500/30'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>}
|
||||||
|
|
||||||
|
<main className="max-w-lg mx-auto px-4 py-6">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="card p-5 animate-pulse">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-10 h-10 rounded-xl bg-gray-200 dark:bg-gray-700" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="h-5 bg-gray-200 dark:bg-gray-700 rounded-lg w-3/4 mb-2" />
|
||||||
|
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded-lg w-1/4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : tasks.length === 0 ? (
|
||||||
|
<motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} className="card p-10 text-center">
|
||||||
|
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-primary-100 to-primary-200 dark:from-primary-900/30 dark:to-primary-800/30 flex items-center justify-center mx-auto mb-5">
|
||||||
|
<Check className="w-10 h-10 text-primary-600 dark:text-primary-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-display font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
{filter === 'active' ? 'Нет активных задач' : <span class="branch-1 cbranch-no" title="branch not covered" >filter === 'completed' ? 'Нет выполненных задач' : 'Нет задач'}</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mb-6">
|
||||||
|
{filter === 'active' ? 'Добавь новую задачу или выбери другой фильтр' : <span class="branch-1 cbranch-no" title="branch not covered" >'Выполняй задачи и они появятся здесь'}</span>
|
||||||
|
</p>
|
||||||
|
{filter === 'active' && (
|
||||||
|
<button <span class="fstat-no" title="function not covered" >onClick={() => <span class="cstat-no" title="statement not covered" >s</span>etShowCreate(true)}</span> className="btn btn-primary">
|
||||||
|
<Plus size={18} />
|
||||||
|
Добавить задачу
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<AnimatePresence>
|
||||||
|
{tasks.map((task, index) => (
|
||||||
|
<TaskCard key={task.id} task={task} index={index} <span class="fstat-no" title="function not covered" >onToggle={() => <span class="cstat-no" title="statement not covered" >h</span>andleToggle(task)}</span> <span class="fstat-no" title="function not covered" >onEdit={() => <span class="cstat-no" title="statement not covered" >s</span>etEditingTask(task)}</span> isLoading={completeMutation.isPending || uncompleteMutation.isPending} />
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{!embedded && <Navigation />}
|
||||||
|
<CreateTaskModal open={showCreate} <span class="fstat-no" title="function not covered" >onClose={() => <span class="cstat-no" title="statement not covered" >s</span>etShowCreate(false)} /></span>
|
||||||
|
<EditTaskModal open={!!editingTask} <span class="fstat-no" title="function not covered" >onClose={() => <span class="cstat-no" title="statement not covered" >s</span>etEditingTask(null)}</span> task={editingTask} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TaskCard({ task, index, onToggle, onEdit, isLoading }) {
|
||||||
|
const [showConfetti, setShowConfetti] = useState(false)
|
||||||
|
const priorityInfo = PRIORITY_LABELS[task.priority]
|
||||||
|
const dueDateLabel = formatDueDate(task.due_date)
|
||||||
|
const isOverdue = task.due_date && isPast(parseISO(task.due_date)) && <span class="branch-2 cbranch-no" title="branch not covered" >!isToday(parseISO(task.due_date)) </span>&& <span class="branch-3 cbranch-no" title="branch not covered" >!task.completed</span>
|
||||||
|
|
||||||
|
const <span class="fstat-no" title="function not covered" >handleCheck = (e</span>) => {
|
||||||
|
<span class="cstat-no" title="statement not covered" > e.stopPropagation()</span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (isLoading) <span class="cstat-no" title="statement not covered" >return</span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > if (!task.completed) { <span class="cstat-no" title="statement not covered" >setShowConfetti(true); <span class="cstat-no" title="statement not covered" ><span class="fstat-no" title="function not covered" >s</span>etTimeout(() => <span class="cstat-no" title="statement not covered" >s</span>etShowConfetti(false),</span> 1000) }</span></span>
|
||||||
|
<span class="cstat-no" title="statement not covered" > onToggle()</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -100 }}
|
||||||
|
transition={{ delay: index * 0.05 }}
|
||||||
|
className="card p-4 relative overflow-hidden"
|
||||||
|
>
|
||||||
|
{showConfetti && (
|
||||||
|
<span class="branch-1 cbranch-no" title="branch not covered" > <motion.div initial={{ opacity: 1 }} animate={{ opacity: 0 }} transition={{ duration: 1 }} className="absolute inset-0 pointer-events-none"></span>
|
||||||
|
{[...Array(6)].<span class="fstat-no" title="function not covered" >map((_</span>, i) => (
|
||||||
|
<span class="cstat-no" title="statement not covered" > <motion.div key={i} initial={{ x: '50%', y: '50%', scale: 0 }} animate={{ x: `${Math.random() * 100}%`, y: `${Math.random() * 100}%`, scale: [0, 1, 0] }} transition={{ duration: 0.6, delay: i * 0.05 }} className="absolute w-2 h-2 rounded-full" style={{ backgroundColor: task.color }} /></span>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<motion.button
|
||||||
|
onClick={handleCheck}
|
||||||
|
disabled={isLoading}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
className={clsx(
|
||||||
|
'w-10 h-10 rounded-xl flex items-center justify-center transition-all duration-300 flex-shrink-0 mt-0.5',
|
||||||
|
task.completed ? <span class="branch-0 cbranch-no" title="branch not covered" >'bg-gradient-to-br from-green-400 to-green-500 shadow-lg shadow-green-400/30' : '</span>border-2 hover:shadow-md'
|
||||||
|
)}
|
||||||
|
style={{ borderColor: task.completed ? <span class="branch-0 cbranch-no" title="branch not covered" >undefined : t</span>ask.color + '40', backgroundColor: task.completed ? <span class="branch-0 cbranch-no" title="branch not covered" >undefined : t</span>ask.color + '10' }}
|
||||||
|
>
|
||||||
|
{task.completed ? (
|
||||||
|
<span class="branch-0 cbranch-no" title="branch not covered" > <motion.div initial={{ scale: 0, rotate: -180 }} animate={{ scale: 1, rotate: 0 }} transition={{ type: 'spring', stiffness: 500 }}></span>
|
||||||
|
<Check className="w-5 h-5 text-white" strokeWidth={3} />
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<span className="text-lg">{task.icon || <span class="branch-1 cbranch-no" title="branch not covered" >'📋'}</span></span>
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0" onClick={onEdit}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className={clsx("font-semibold truncate cursor-pointer hover:text-primary-600 dark:hover:text-primary-400", task.completed ? <span class="branch-0 cbranch-no" title="branch not covered" >"text-gray-400 line-through" : "</span>text-gray-900 dark:text-white")}>{task.title}</h3>
|
||||||
|
{task.is_recurring && <span class="branch-1 cbranch-no" title="branch not covered" ><span className="text-sm" title={RECURRENCE_LABELS[task.recurrence_type] || 'Повторяется'}>🔄</span>}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{task.description && <span class="branch-1 cbranch-no" title="branch not covered" ><p className="text-sm text-gray-500 dark:text-gray-400 truncate mt-0.5">{task.description}</p>}</span>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mt-2 flex-wrap">
|
||||||
|
{dueDateLabel && (
|
||||||
|
<span className={clsx('inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium', isOverdue ? <span class="branch-0 cbranch-no" title="branch not covered" >'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' : '</span>bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400')}>
|
||||||
|
{isOverdue && <span class="branch-1 cbranch-no" title="branch not covered" ><AlertTriangle size={12} />}</span>
|
||||||
|
<Calendar size={12} />
|
||||||
|
{dueDateLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{priorityInfo && <span className={clsx('px-2 py-0.5 rounded-md text-xs font-medium', priorityInfo.class)}>{priorityInfo.label}</span>}
|
||||||
|
|
||||||
|
{task.is_recurring && <span class="branch-1 cbranch-no" title="branch not covered" >task.recurrence_type && (</span>
|
||||||
|
<span class="branch-2 cbranch-no" title="branch not covered" > <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs font-medium bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400"></span>
|
||||||
|
<Repeat size={12} />
|
||||||
|
{RECURRENCE_LABELS[task.recurrence_type]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button onClick={onEdit} className="p-2 text-gray-400 hover:text-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-xl transition-all">
|
||||||
|
<Edit2 size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{task.completed && (
|
||||||
|
<span class="branch-1 cbranch-no" title="branch not covered" > <motion.button initial={{ opacity: 0, scale: 0.8 }} animate={{ opacity: 1, scale: 1 }} onClick={handleCheck} disabled={isLoading} className="p-2 text-gray-400 hover:text-orange-500 hover:bg-orange-50 dark:hover:bg-orange-900/20 rounded-xl transition-all" title="Отменить"></span>
|
||||||
|
<Undo2 size={16} />
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
241
coverage/lcov-report/pages/Tracker.jsx.html
Normal file
241
coverage/lcov-report/pages/Tracker.jsx.html
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/Tracker.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> Tracker.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>5/5</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>8/8</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>3/3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>5/5</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</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>
|
||||||
|
<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">1x</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">11x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</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">33x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">4x</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></td><td class="text"><pre class="prettyprint lang-js">import { useState, lazy, Suspense } from "react"
|
||||||
|
import Navigation from "../components/Navigation"
|
||||||
|
|
||||||
|
// Import pages as components (they render their own content but we strip their Navigation)
|
||||||
|
import HabitsContent from "./Habits"
|
||||||
|
import TasksContent from "./Tasks"
|
||||||
|
import StatsContent from "./Stats"
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: "habits", label: "Привычки", icon: "🎯" },
|
||||||
|
{ key: "tasks", label: "Задачи", icon: "✅" },
|
||||||
|
{ key: "stats", label: "Статистика", icon: "📊" },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function Tracker() {
|
||||||
|
const [activeTab, setActiveTab] = useState("habits")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-surface-50 dark:bg-gray-950 gradient-mesh pb-24">
|
||||||
|
<header className="bg-white/70 dark:bg-gray-900/70 backdrop-blur-xl border-b border-gray-100/50 dark:border-gray-800 sticky top-0 z-10">
|
||||||
|
<div className="max-w-lg mx-auto px-4 py-4">
|
||||||
|
<h1 className="text-xl font-display font-bold text-gray-900 dark:text-white">
|
||||||
|
📊 Трекер
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-lg mx-auto px-4 pb-3 flex gap-2">
|
||||||
|
{tabs.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.key}
|
||||||
|
onClick={() => setActiveTab(t.key)}
|
||||||
|
className={`flex-1 py-2 rounded-xl text-sm font-semibold transition ${
|
||||||
|
activeTab === t.key
|
||||||
|
? "bg-primary-500 text-white"
|
||||||
|
: "bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t.icon} {t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{activeTab === "habits" && <HabitsContent embedded />}
|
||||||
|
{activeTab === "tasks" && <TasksContent embedded />}
|
||||||
|
{activeTab === "stats" && <StatsContent embedded />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Navigation />
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
394
coverage/lcov-report/pages/VerifyEmail.jsx.html
Normal file
394
coverage/lcov-report/pages/VerifyEmail.jsx.html
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages/VerifyEmail.jsx</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">pages</a> VerifyEmail.jsx</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>18/18</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">90% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>9/10</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>3/3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>18/18</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</a>
|
||||||
|
<a name='L49'></a><a href='#L49'>49</a>
|
||||||
|
<a name='L50'></a><a href='#L50'>50</a>
|
||||||
|
<a name='L51'></a><a href='#L51'>51</a>
|
||||||
|
<a name='L52'></a><a href='#L52'>52</a>
|
||||||
|
<a name='L53'></a><a href='#L53'>53</a>
|
||||||
|
<a name='L54'></a><a href='#L54'>54</a>
|
||||||
|
<a name='L55'></a><a href='#L55'>55</a>
|
||||||
|
<a name='L56'></a><a href='#L56'>56</a>
|
||||||
|
<a name='L57'></a><a href='#L57'>57</a>
|
||||||
|
<a name='L58'></a><a href='#L58'>58</a>
|
||||||
|
<a name='L59'></a><a href='#L59'>59</a>
|
||||||
|
<a name='L60'></a><a href='#L60'>60</a>
|
||||||
|
<a name='L61'></a><a href='#L61'>61</a>
|
||||||
|
<a name='L62'></a><a href='#L62'>62</a>
|
||||||
|
<a name='L63'></a><a href='#L63'>63</a>
|
||||||
|
<a name='L64'></a><a href='#L64'>64</a>
|
||||||
|
<a name='L65'></a><a href='#L65'>65</a>
|
||||||
|
<a name='L66'></a><a href='#L66'>66</a>
|
||||||
|
<a name='L67'></a><a href='#L67'>67</a>
|
||||||
|
<a name='L68'></a><a href='#L68'>68</a>
|
||||||
|
<a name='L69'></a><a href='#L69'>69</a>
|
||||||
|
<a name='L70'></a><a href='#L70'>70</a>
|
||||||
|
<a name='L71'></a><a href='#L71'>71</a>
|
||||||
|
<a name='L72'></a><a href='#L72'>72</a>
|
||||||
|
<a name='L73'></a><a href='#L73'>73</a>
|
||||||
|
<a name='L74'></a><a href='#L74'>74</a>
|
||||||
|
<a name='L75'></a><a href='#L75'>75</a>
|
||||||
|
<a name='L76'></a><a href='#L76'>76</a>
|
||||||
|
<a name='L77'></a><a href='#L77'>77</a>
|
||||||
|
<a name='L78'></a><a href='#L78'>78</a>
|
||||||
|
<a name='L79'></a><a href='#L79'>79</a>
|
||||||
|
<a name='L80'></a><a href='#L80'>80</a>
|
||||||
|
<a name='L81'></a><a href='#L81'>81</a>
|
||||||
|
<a name='L82'></a><a href='#L82'>82</a>
|
||||||
|
<a name='L83'></a><a href='#L83'>83</a>
|
||||||
|
<a name='L84'></a><a href='#L84'>84</a>
|
||||||
|
<a name='L85'></a><a href='#L85'>85</a>
|
||||||
|
<a name='L86'></a><a href='#L86'>86</a>
|
||||||
|
<a name='L87'></a><a href='#L87'>87</a>
|
||||||
|
<a name='L88'></a><a href='#L88'>88</a>
|
||||||
|
<a name='L89'></a><a href='#L89'>89</a>
|
||||||
|
<a name='L90'></a><a href='#L90'>90</a>
|
||||||
|
<a name='L91'></a><a href='#L91'>91</a>
|
||||||
|
<a name='L92'></a><a href='#L92'>92</a>
|
||||||
|
<a name='L93'></a><a href='#L93'>93</a>
|
||||||
|
<a name='L94'></a><a href='#L94'>94</a>
|
||||||
|
<a name='L95'></a><a href='#L95'>95</a>
|
||||||
|
<a name='L96'></a><a href='#L96'>96</a>
|
||||||
|
<a name='L97'></a><a href='#L97'>97</a>
|
||||||
|
<a name='L98'></a><a href='#L98'>98</a>
|
||||||
|
<a name='L99'></a><a href='#L99'>99</a>
|
||||||
|
<a name='L100'></a><a href='#L100'>100</a>
|
||||||
|
<a name='L101'></a><a href='#L101'>101</a>
|
||||||
|
<a name='L102'></a><a href='#L102'>102</a>
|
||||||
|
<a name='L103'></a><a href='#L103'>103</a>
|
||||||
|
<a name='L104'></a><a href='#L104'>104</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>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">11x</span>
|
||||||
|
<span class="cline-any cline-yes">6x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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-yes">5x</span>
|
||||||
|
<span class="cline-any cline-yes">5x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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>
|
||||||
|
<span class="cline-any cline-yes">11x</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-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-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></td><td class="text"><pre class="prettyprint lang-js">import { useEffect, useState } from 'react'
|
||||||
|
import { useSearchParams, Link } from 'react-router-dom'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { CheckCircle, XCircle, Loader2, Zap } from 'lucide-react'
|
||||||
|
import api from '../api/client'
|
||||||
|
|
||||||
|
export default function VerifyEmail() {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const [status, setStatus] = useState('loading') // loading, success, error
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
|
||||||
|
const token = searchParams.get('token')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!token) {
|
||||||
|
setStatus('error')
|
||||||
|
setMessage('Токен не найден')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const verify = async () => {
|
||||||
|
try {
|
||||||
|
await api.post('/auth/verify-email', { token })
|
||||||
|
setStatus('success')
|
||||||
|
setMessage('Email успешно подтверждён!')
|
||||||
|
} catch (err) {
|
||||||
|
setStatus('error')
|
||||||
|
setMessage(err.response?.data?.error || <span class="branch-1 cbranch-no" title="branch not covered" >'Ошибка верификации')</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verify()
|
||||||
|
}, [token])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center p-4 gradient-mesh bg-surface-50">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="w-full max-w-md"
|
||||||
|
>
|
||||||
|
<div className="card p-10 text-center">
|
||||||
|
{status === 'loading' && (
|
||||||
|
<>
|
||||||
|
<div className="w-20 h-20 rounded-3xl bg-primary-100 flex items-center justify-center mx-auto mb-6">
|
||||||
|
<Loader2 className="w-10 h-10 text-primary-600 animate-spin" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-display font-bold text-gray-900 mb-2">
|
||||||
|
Проверяем...
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500">Подожди секунду</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'success' && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 200 }}
|
||||||
|
className="w-20 h-20 rounded-3xl bg-green-100 flex items-center justify-center mx-auto mb-6"
|
||||||
|
>
|
||||||
|
<CheckCircle className="w-10 h-10 text-green-600" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-2xl font-display font-bold text-gray-900 mb-2">
|
||||||
|
Готово! 🎉
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mb-6">{message}</p>
|
||||||
|
<Link to="/login" className="btn btn-primary">
|
||||||
|
Войти в аккаунт
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'error' && (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 200 }}
|
||||||
|
className="w-20 h-20 rounded-3xl bg-red-100 flex items-center justify-center mx-auto mb-6"
|
||||||
|
>
|
||||||
|
<XCircle className="w-10 h-10 text-red-600" />
|
||||||
|
</motion.div>
|
||||||
|
<h1 className="text-2xl font-display font-bold text-gray-900 mb-2">
|
||||||
|
Ошибка
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 mb-6">{message}</p>
|
||||||
|
<Link to="/login" className="btn btn-secondary">
|
||||||
|
На главную
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-6 text-gray-400">
|
||||||
|
<Zap size={16} />
|
||||||
|
<span className="text-sm font-medium">Pulse</span>
|
||||||
|
</div>
|
||||||
|
</motion.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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
296
coverage/lcov-report/pages/index.html
Normal file
296
coverage/lcov-report/pages/index.html
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for pages</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> pages</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">46.79% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>452/966</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">40.62% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>325/800</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">32.11% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>105/327</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">50.95% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>427/838</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line low'></div>
|
||||||
|
<div class="pad1">
|
||||||
|
<table class="coverage-summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||||
|
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||||
|
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||||
|
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||||
|
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||||
|
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||||
|
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="file low" data-value="Finance.jsx"><a href="Finance.jsx.html">Finance.jsx</a></td>
|
||||||
|
<td data-value="44.44" class="pic low">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 44%"></div><div class="cover-empty" style="width: 56%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="44.44" class="pct low">44.44%</td>
|
||||||
|
<td data-value="36" class="abs low">16/36</td>
|
||||||
|
<td data-value="75" class="pct medium">75%</td>
|
||||||
|
<td data-value="20" class="abs medium">15/20</td>
|
||||||
|
<td data-value="25" class="pct low">25%</td>
|
||||||
|
<td data-value="16" class="abs low">4/16</td>
|
||||||
|
<td data-value="64" class="pct medium">64%</td>
|
||||||
|
<td data-value="25" class="abs medium">16/25</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="ForgotPassword.jsx"><a href="ForgotPassword.jsx.html">ForgotPassword.jsx</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="17" class="abs high">17/17</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="8" class="abs high">8/8</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="3" class="abs high">3/3</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="17" class="abs high">17/17</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="Habits.jsx"><a href="Habits.jsx.html">Habits.jsx</a></td>
|
||||||
|
<td data-value="64.91" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 64%"></div><div class="cover-empty" style="width: 36%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="64.91" class="pct medium">64.91%</td>
|
||||||
|
<td data-value="57" class="abs medium">37/57</td>
|
||||||
|
<td data-value="65.9" class="pct medium">65.9%</td>
|
||||||
|
<td data-value="44" class="abs medium">29/44</td>
|
||||||
|
<td data-value="53.57" class="pct medium">53.57%</td>
|
||||||
|
<td data-value="28" class="abs medium">15/28</td>
|
||||||
|
<td data-value="65.21" class="pct medium">65.21%</td>
|
||||||
|
<td data-value="46" class="abs medium">30/46</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file low" data-value="Home.jsx"><a href="Home.jsx.html">Home.jsx</a></td>
|
||||||
|
<td data-value="17.46" class="pic low">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 17%"></div><div class="cover-empty" style="width: 83%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="17.46" class="pct low">17.46%</td>
|
||||||
|
<td data-value="189" class="abs low">33/189</td>
|
||||||
|
<td data-value="8.84" class="pct low">8.84%</td>
|
||||||
|
<td data-value="147" class="abs low">13/147</td>
|
||||||
|
<td data-value="9.25" class="pct low">9.25%</td>
|
||||||
|
<td data-value="54" class="abs low">5/54</td>
|
||||||
|
<td data-value="21.15" class="pct low">21.15%</td>
|
||||||
|
<td data-value="156" class="abs low">33/156</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="Login.jsx"><a href="Login.jsx.html">Login.jsx</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="21" class="abs high">21/21</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="10" class="abs high">10/10</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="6" class="abs high">6/6</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="20" class="abs high">20/20</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="Register.jsx"><a href="Register.jsx.html">Register.jsx</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="23" class="abs high">23/23</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="10" class="abs high">10/10</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="7" class="abs high">7/7</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="22" class="abs high">22/22</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="ResetPassword.jsx"><a href="ResetPassword.jsx.html">ResetPassword.jsx</a></td>
|
||||||
|
<td data-value="96.29" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 96%"></div><div class="cover-empty" style="width: 4%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="96.29" class="pct high">96.29%</td>
|
||||||
|
<td data-value="27" class="abs high">26/27</td>
|
||||||
|
<td data-value="92.85" class="pct high">92.85%</td>
|
||||||
|
<td data-value="14" class="abs high">13/14</td>
|
||||||
|
<td data-value="80" class="pct high">80%</td>
|
||||||
|
<td data-value="5" class="abs high">4/5</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="26" class="abs high">26/26</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file low" data-value="Savings.jsx"><a href="Savings.jsx.html">Savings.jsx</a></td>
|
||||||
|
<td data-value="15.88" class="pic low">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 15%"></div><div class="cover-empty" style="width: 85%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="15.88" class="pct low">15.88%</td>
|
||||||
|
<td data-value="277" class="abs low">44/277</td>
|
||||||
|
<td data-value="14.07" class="pct low">14.07%</td>
|
||||||
|
<td data-value="270" class="abs low">38/270</td>
|
||||||
|
<td data-value="9.09" class="pct low">9.09%</td>
|
||||||
|
<td data-value="121" class="abs low">11/121</td>
|
||||||
|
<td data-value="17.4" class="pct low">17.4%</td>
|
||||||
|
<td data-value="247" class="abs low">43/247</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="Settings.jsx"><a href="Settings.jsx.html">Settings.jsx</a></td>
|
||||||
|
<td data-value="81.63" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 81%"></div><div class="cover-empty" style="width: 19%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="81.63" class="pct high">81.63%</td>
|
||||||
|
<td data-value="49" class="abs high">40/49</td>
|
||||||
|
<td data-value="78.18" class="pct medium">78.18%</td>
|
||||||
|
<td data-value="55" class="abs medium">43/55</td>
|
||||||
|
<td data-value="50" class="pct medium">50%</td>
|
||||||
|
<td data-value="14" class="abs medium">7/14</td>
|
||||||
|
<td data-value="83.33" class="pct high">83.33%</td>
|
||||||
|
<td data-value="48" class="abs high">40/48</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="Stats.jsx"><a href="Stats.jsx.html">Stats.jsx</a></td>
|
||||||
|
<td data-value="75.39" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 75%"></div><div class="cover-empty" style="width: 25%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="75.39" class="pct medium">75.39%</td>
|
||||||
|
<td data-value="191" class="abs medium">144/191</td>
|
||||||
|
<td data-value="65.89" class="pct medium">65.89%</td>
|
||||||
|
<td data-value="129" class="abs medium">85/129</td>
|
||||||
|
<td data-value="66.66" class="pct medium">66.66%</td>
|
||||||
|
<td data-value="45" class="abs medium">30/45</td>
|
||||||
|
<td data-value="79.75" class="pct medium">79.75%</td>
|
||||||
|
<td data-value="163" class="abs medium">130/163</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file medium" data-value="Tasks.jsx"><a href="Tasks.jsx.html">Tasks.jsx</a></td>
|
||||||
|
<td data-value="50" class="pic medium">
|
||||||
|
<div class="chart"><div class="cover-fill" style="width: 50%"></div><div class="cover-empty" style="width: 50%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="50" class="pct medium">50%</td>
|
||||||
|
<td data-value="56" class="abs medium">28/56</td>
|
||||||
|
<td data-value="58.66" class="pct medium">58.66%</td>
|
||||||
|
<td data-value="75" class="abs medium">44/75</td>
|
||||||
|
<td data-value="31.81" class="pct low">31.81%</td>
|
||||||
|
<td data-value="22" class="abs low">7/22</td>
|
||||||
|
<td data-value="60" class="pct medium">60%</td>
|
||||||
|
<td data-value="45" class="abs medium">27/45</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="Tracker.jsx"><a href="Tracker.jsx.html">Tracker.jsx</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="5" class="abs high">5/5</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="8" class="abs high">8/8</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="3" class="abs high">3/3</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="5" class="abs high">5/5</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td class="file high" data-value="VerifyEmail.jsx"><a href="VerifyEmail.jsx.html">VerifyEmail.jsx</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="18" class="abs high">18/18</td>
|
||||||
|
<td data-value="90" class="pct high">90%</td>
|
||||||
|
<td data-value="10" class="abs high">9/10</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="3" class="abs high">3/3</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="18" class="abs high">18/18</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
1
coverage/lcov-report/prettify.css
Normal file
1
coverage/lcov-report/prettify.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
|
||||||
2
coverage/lcov-report/prettify.js
Normal file
2
coverage/lcov-report/prettify.js
Normal file
File diff suppressed because one or more lines are too long
BIN
coverage/lcov-report/sort-arrow-sprite.png
Normal file
BIN
coverage/lcov-report/sort-arrow-sprite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 B |
210
coverage/lcov-report/sorter.js
Normal file
210
coverage/lcov-report/sorter.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
var addSorting = (function() {
|
||||||
|
'use strict';
|
||||||
|
var cols,
|
||||||
|
currentSort = {
|
||||||
|
index: 0,
|
||||||
|
desc: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// returns the summary table element
|
||||||
|
function getTable() {
|
||||||
|
return document.querySelector('.coverage-summary');
|
||||||
|
}
|
||||||
|
// returns the thead element of the summary table
|
||||||
|
function getTableHeader() {
|
||||||
|
return getTable().querySelector('thead tr');
|
||||||
|
}
|
||||||
|
// returns the tbody element of the summary table
|
||||||
|
function getTableBody() {
|
||||||
|
return getTable().querySelector('tbody');
|
||||||
|
}
|
||||||
|
// returns the th element for nth column
|
||||||
|
function getNthColumn(n) {
|
||||||
|
return getTableHeader().querySelectorAll('th')[n];
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilterInput() {
|
||||||
|
const searchValue = document.getElementById('fileSearch').value;
|
||||||
|
const rows = document.getElementsByTagName('tbody')[0].children;
|
||||||
|
|
||||||
|
// Try to create a RegExp from the searchValue. If it fails (invalid regex),
|
||||||
|
// it will be treated as a plain text search
|
||||||
|
let searchRegex;
|
||||||
|
try {
|
||||||
|
searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive
|
||||||
|
} catch (error) {
|
||||||
|
searchRegex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const row = rows[i];
|
||||||
|
let isMatch = false;
|
||||||
|
|
||||||
|
if (searchRegex) {
|
||||||
|
// If a valid regex was created, use it for matching
|
||||||
|
isMatch = searchRegex.test(row.textContent);
|
||||||
|
} else {
|
||||||
|
// Otherwise, fall back to the original plain text search
|
||||||
|
isMatch = row.textContent
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchValue.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
row.style.display = isMatch ? '' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads the search box
|
||||||
|
function addSearchBox() {
|
||||||
|
var template = document.getElementById('filterTemplate');
|
||||||
|
var templateClone = template.content.cloneNode(true);
|
||||||
|
templateClone.getElementById('fileSearch').oninput = onFilterInput;
|
||||||
|
template.parentElement.appendChild(templateClone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// loads all columns
|
||||||
|
function loadColumns() {
|
||||||
|
var colNodes = getTableHeader().querySelectorAll('th'),
|
||||||
|
colNode,
|
||||||
|
cols = [],
|
||||||
|
col,
|
||||||
|
i;
|
||||||
|
|
||||||
|
for (i = 0; i < colNodes.length; i += 1) {
|
||||||
|
colNode = colNodes[i];
|
||||||
|
col = {
|
||||||
|
key: colNode.getAttribute('data-col'),
|
||||||
|
sortable: !colNode.getAttribute('data-nosort'),
|
||||||
|
type: colNode.getAttribute('data-type') || 'string'
|
||||||
|
};
|
||||||
|
cols.push(col);
|
||||||
|
if (col.sortable) {
|
||||||
|
col.defaultDescSort = col.type === 'number';
|
||||||
|
colNode.innerHTML =
|
||||||
|
colNode.innerHTML + '<span class="sorter"></span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cols;
|
||||||
|
}
|
||||||
|
// attaches a data attribute to every tr element with an object
|
||||||
|
// of data values keyed by column name
|
||||||
|
function loadRowData(tableRow) {
|
||||||
|
var tableCols = tableRow.querySelectorAll('td'),
|
||||||
|
colNode,
|
||||||
|
col,
|
||||||
|
data = {},
|
||||||
|
i,
|
||||||
|
val;
|
||||||
|
for (i = 0; i < tableCols.length; i += 1) {
|
||||||
|
colNode = tableCols[i];
|
||||||
|
col = cols[i];
|
||||||
|
val = colNode.getAttribute('data-value');
|
||||||
|
if (col.type === 'number') {
|
||||||
|
val = Number(val);
|
||||||
|
}
|
||||||
|
data[col.key] = val;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
// loads all row data
|
||||||
|
function loadData() {
|
||||||
|
var rows = getTableBody().querySelectorAll('tr'),
|
||||||
|
i;
|
||||||
|
|
||||||
|
for (i = 0; i < rows.length; i += 1) {
|
||||||
|
rows[i].data = loadRowData(rows[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// sorts the table using the data for the ith column
|
||||||
|
function sortByIndex(index, desc) {
|
||||||
|
var key = cols[index].key,
|
||||||
|
sorter = function(a, b) {
|
||||||
|
a = a.data[key];
|
||||||
|
b = b.data[key];
|
||||||
|
return a < b ? -1 : a > b ? 1 : 0;
|
||||||
|
},
|
||||||
|
finalSorter = sorter,
|
||||||
|
tableBody = document.querySelector('.coverage-summary tbody'),
|
||||||
|
rowNodes = tableBody.querySelectorAll('tr'),
|
||||||
|
rows = [],
|
||||||
|
i;
|
||||||
|
|
||||||
|
if (desc) {
|
||||||
|
finalSorter = function(a, b) {
|
||||||
|
return -1 * sorter(a, b);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < rowNodes.length; i += 1) {
|
||||||
|
rows.push(rowNodes[i]);
|
||||||
|
tableBody.removeChild(rowNodes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.sort(finalSorter);
|
||||||
|
|
||||||
|
for (i = 0; i < rows.length; i += 1) {
|
||||||
|
tableBody.appendChild(rows[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// removes sort indicators for current column being sorted
|
||||||
|
function removeSortIndicators() {
|
||||||
|
var col = getNthColumn(currentSort.index),
|
||||||
|
cls = col.className;
|
||||||
|
|
||||||
|
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
|
||||||
|
col.className = cls;
|
||||||
|
}
|
||||||
|
// adds sort indicators for current column being sorted
|
||||||
|
function addSortIndicators() {
|
||||||
|
getNthColumn(currentSort.index).className += currentSort.desc
|
||||||
|
? ' sorted-desc'
|
||||||
|
: ' sorted';
|
||||||
|
}
|
||||||
|
// adds event listeners for all sorter widgets
|
||||||
|
function enableUI() {
|
||||||
|
var i,
|
||||||
|
el,
|
||||||
|
ithSorter = function ithSorter(i) {
|
||||||
|
var col = cols[i];
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
var desc = col.defaultDescSort;
|
||||||
|
|
||||||
|
if (currentSort.index === i) {
|
||||||
|
desc = !currentSort.desc;
|
||||||
|
}
|
||||||
|
sortByIndex(i, desc);
|
||||||
|
removeSortIndicators();
|
||||||
|
currentSort.index = i;
|
||||||
|
currentSort.desc = desc;
|
||||||
|
addSortIndicators();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
for (i = 0; i < cols.length; i += 1) {
|
||||||
|
if (cols[i].sortable) {
|
||||||
|
// add the click event handler on the th so users
|
||||||
|
// dont have to click on those tiny arrows
|
||||||
|
el = getNthColumn(i).querySelector('.sorter').parentElement;
|
||||||
|
if (el.addEventListener) {
|
||||||
|
el.addEventListener('click', ithSorter(i));
|
||||||
|
} else {
|
||||||
|
el.attachEvent('onclick', ithSorter(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// adds sorting functionality to the UI
|
||||||
|
return function() {
|
||||||
|
if (!getTable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cols = loadColumns();
|
||||||
|
loadData();
|
||||||
|
addSearchBox();
|
||||||
|
addSortIndicators();
|
||||||
|
enableUI();
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
window.addEventListener('load', addSorting);
|
||||||
226
coverage/lcov-report/store/auth.js.html
Normal file
226
coverage/lcov-report/store/auth.js.html
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for store/auth.js</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> / <a href="index.html">store</a> auth.js</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>25/25</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>2/2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>5/5</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>24/24</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></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>
|
||||||
|
<a name='L4'></a><a href='#L4'>4</a>
|
||||||
|
<a name='L5'></a><a href='#L5'>5</a>
|
||||||
|
<a name='L6'></a><a href='#L6'>6</a>
|
||||||
|
<a name='L7'></a><a href='#L7'>7</a>
|
||||||
|
<a name='L8'></a><a href='#L8'>8</a>
|
||||||
|
<a name='L9'></a><a href='#L9'>9</a>
|
||||||
|
<a name='L10'></a><a href='#L10'>10</a>
|
||||||
|
<a name='L11'></a><a href='#L11'>11</a>
|
||||||
|
<a name='L12'></a><a href='#L12'>12</a>
|
||||||
|
<a name='L13'></a><a href='#L13'>13</a>
|
||||||
|
<a name='L14'></a><a href='#L14'>14</a>
|
||||||
|
<a name='L15'></a><a href='#L15'>15</a>
|
||||||
|
<a name='L16'></a><a href='#L16'>16</a>
|
||||||
|
<a name='L17'></a><a href='#L17'>17</a>
|
||||||
|
<a name='L18'></a><a href='#L18'>18</a>
|
||||||
|
<a name='L19'></a><a href='#L19'>19</a>
|
||||||
|
<a name='L20'></a><a href='#L20'>20</a>
|
||||||
|
<a name='L21'></a><a href='#L21'>21</a>
|
||||||
|
<a name='L22'></a><a href='#L22'>22</a>
|
||||||
|
<a name='L23'></a><a href='#L23'>23</a>
|
||||||
|
<a name='L24'></a><a href='#L24'>24</a>
|
||||||
|
<a name='L25'></a><a href='#L25'>25</a>
|
||||||
|
<a name='L26'></a><a href='#L26'>26</a>
|
||||||
|
<a name='L27'></a><a href='#L27'>27</a>
|
||||||
|
<a name='L28'></a><a href='#L28'>28</a>
|
||||||
|
<a name='L29'></a><a href='#L29'>29</a>
|
||||||
|
<a name='L30'></a><a href='#L30'>30</a>
|
||||||
|
<a name='L31'></a><a href='#L31'>31</a>
|
||||||
|
<a name='L32'></a><a href='#L32'>32</a>
|
||||||
|
<a name='L33'></a><a href='#L33'>33</a>
|
||||||
|
<a name='L34'></a><a href='#L34'>34</a>
|
||||||
|
<a name='L35'></a><a href='#L35'>35</a>
|
||||||
|
<a name='L36'></a><a href='#L36'>36</a>
|
||||||
|
<a name='L37'></a><a href='#L37'>37</a>
|
||||||
|
<a name='L38'></a><a href='#L38'>38</a>
|
||||||
|
<a name='L39'></a><a href='#L39'>39</a>
|
||||||
|
<a name='L40'></a><a href='#L40'>40</a>
|
||||||
|
<a name='L41'></a><a href='#L41'>41</a>
|
||||||
|
<a name='L42'></a><a href='#L42'>42</a>
|
||||||
|
<a name='L43'></a><a href='#L43'>43</a>
|
||||||
|
<a name='L44'></a><a href='#L44'>44</a>
|
||||||
|
<a name='L45'></a><a href='#L45'>45</a>
|
||||||
|
<a name='L46'></a><a href='#L46'>46</a>
|
||||||
|
<a name='L47'></a><a href='#L47'>47</a>
|
||||||
|
<a name='L48'></a><a href='#L48'>48</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-yes">1x</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">3x</span>
|
||||||
|
<span class="cline-any cline-yes">3x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">2x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">2x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</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">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-yes">1x</span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span>
|
||||||
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import { create } from 'zustand'
|
||||||
|
import api from '../api/client'
|
||||||
|
|
||||||
|
export const useAuthStore = create((set, get) => ({
|
||||||
|
user: null,
|
||||||
|
isLoading: true,
|
||||||
|
isAuthenticated: false,
|
||||||
|
|
||||||
|
initialize: async () => {
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
if (!token) {
|
||||||
|
set({ isLoading: false, isAuthenticated: false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await api.get('/auth/me')
|
||||||
|
set({ user: data, isLoading: false, isAuthenticated: true })
|
||||||
|
} catch (error) {
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('refresh_token')
|
||||||
|
set({ user: null, isLoading: false, isAuthenticated: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
login: async (email, password) => {
|
||||||
|
const { data } = await api.post('/auth/login', { email, password })
|
||||||
|
localStorage.setItem('access_token', data.access_token)
|
||||||
|
localStorage.setItem('refresh_token', data.refresh_token)
|
||||||
|
set({ user: data.user, isAuthenticated: true })
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
register: async (email, username, password) => {
|
||||||
|
const { data } = await api.post('/auth/register', { email, username, password })
|
||||||
|
localStorage.setItem('access_token', data.access_token)
|
||||||
|
localStorage.setItem('refresh_token', data.refresh_token)
|
||||||
|
set({ user: data.user, isAuthenticated: true })
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('access_token')
|
||||||
|
localStorage.removeItem('refresh_token')
|
||||||
|
set({ user: null, isAuthenticated: false })
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
</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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
116
coverage/lcov-report/store/index.html
Normal file
116
coverage/lcov-report/store/index.html
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>Code coverage report for store</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>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class='wrapper'>
|
||||||
|
<div class='pad1'>
|
||||||
|
<h1><a href="../index.html">All files</a> store</h1>
|
||||||
|
<div class='clearfix'>
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Statements</span>
|
||||||
|
<span class='fraction'>25/25</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Branches</span>
|
||||||
|
<span class='fraction'>2/2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Functions</span>
|
||||||
|
<span class='fraction'>5/5</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class='fl pad1y space-right2'>
|
||||||
|
<span class="strong">100% </span>
|
||||||
|
<span class="quiet">Lines</span>
|
||||||
|
<span class='fraction'>24/24</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.
|
||||||
|
</p>
|
||||||
|
<template id="filterTemplate">
|
||||||
|
<div class="quiet">
|
||||||
|
Filter:
|
||||||
|
<input type="search" id="fileSearch">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class='status-line high'></div>
|
||||||
|
<div class="pad1">
|
||||||
|
<table class="coverage-summary">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
|
||||||
|
<th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
|
||||||
|
<th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
|
||||||
|
<th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
|
||||||
|
<th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
|
||||||
|
<th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
<th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
|
||||||
|
<th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody><tr>
|
||||||
|
<td class="file high" data-value="auth.js"><a href="auth.js.html">auth.js</a></td>
|
||||||
|
<td data-value="100" class="pic high">
|
||||||
|
<div class="chart"><div class="cover-fill cover-full" style="width: 100%"></div><div class="cover-empty" style="width: 0%"></div></div>
|
||||||
|
</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="25" class="abs high">25/25</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="2" class="abs high">2/2</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="5" class="abs high">5/5</td>
|
||||||
|
<td data-value="100" class="pct high">100%</td>
|
||||||
|
<td data-value="24" class="abs high">24/24</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<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-03-26T19:16:45.407Z
|
||||||
|
</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>
|
||||||
|
|
||||||
4213
coverage/lcov.info
Normal file
4213
coverage/lcov.info
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ import ResetPassword from "./pages/ResetPassword"
|
|||||||
import ForgotPassword from "./pages/ForgotPassword"
|
import ForgotPassword from "./pages/ForgotPassword"
|
||||||
import Stats from "./pages/Stats"
|
import Stats from "./pages/Stats"
|
||||||
import Settings from "./pages/Settings"
|
import Settings from "./pages/Settings"
|
||||||
import Finance from "./pages/Finance"
|
|
||||||
import Tracker from "./pages/Tracker"
|
import Tracker from "./pages/Tracker"
|
||||||
|
|
||||||
function ProtectedRoute({ children }) {
|
function ProtectedRoute({ children }) {
|
||||||
@@ -135,14 +134,6 @@ export default function App() {
|
|||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
path="/finance"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute>
|
|
||||||
<Finance />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings"
|
path="/settings"
|
||||||
element={
|
element={
|
||||||
|
|||||||
7
src/__tests__/App.test.jsx
Normal file
7
src/__tests__/App.test.jsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
|
||||||
|
describe('App', () => {
|
||||||
|
it('should pass basic test', () => {
|
||||||
|
expect(1 + 1).toBe(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
100
src/__tests__/CreateHabitModal.test.jsx
Normal file
100
src/__tests__/CreateHabitModal.test.jsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import CreateHabitModal from '../components/CreateHabitModal'
|
||||||
|
|
||||||
|
vi.mock('../api/client', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/habits', () => ({
|
||||||
|
habitsApi: {
|
||||||
|
create: vi.fn(),
|
||||||
|
list: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { habitsApi } from '../api/habits'
|
||||||
|
|
||||||
|
const renderModal = (props = {}) => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<CreateHabitModal open={true} onClose={vi.fn()} {...props} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CreateHabitModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render when open=false', () => {
|
||||||
|
const qc = new QueryClient()
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<CreateHabitModal open={false} onClose={vi.fn()} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
expect(screen.queryByText('Новая привычка')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders form when open=true', () => {
|
||||||
|
renderModal()
|
||||||
|
expect(screen.getByText('Новая привычка')).toBeInTheDocument()
|
||||||
|
// Placeholder is "Например: Пить воду"
|
||||||
|
expect(screen.getByPlaceholderText('Например: Пить воду')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error when submitting empty name', async () => {
|
||||||
|
renderModal()
|
||||||
|
fireEvent.click(screen.getByText('Создать привычку'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Введи название привычки')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits habit successfully', async () => {
|
||||||
|
habitsApi.create.mockResolvedValueOnce({ id: 1, name: 'Exercise' })
|
||||||
|
const onClose = vi.fn()
|
||||||
|
renderModal({ onClose })
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Например: Пить воду'), {
|
||||||
|
target: { value: 'Exercise' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Создать привычку'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(habitsApi.create).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders frequency options', () => {
|
||||||
|
renderModal()
|
||||||
|
expect(screen.getByText('Ежедневно')).toBeInTheDocument()
|
||||||
|
// "По дням" is shown instead of "Еженедельно"
|
||||||
|
expect(screen.getByText('По дням')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onClose when close button clicked', () => {
|
||||||
|
const onClose = vi.fn()
|
||||||
|
renderModal({ onClose })
|
||||||
|
const closeBtn = screen.getAllByRole('button')[0]
|
||||||
|
fireEvent.click(closeBtn)
|
||||||
|
expect(onClose).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders color options', () => {
|
||||||
|
renderModal()
|
||||||
|
// Colors rendered as buttons
|
||||||
|
const buttons = screen.getAllByRole('button')
|
||||||
|
expect(buttons.length).toBeGreaterThan(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
107
src/__tests__/CreateTaskModal.test.jsx
Normal file
107
src/__tests__/CreateTaskModal.test.jsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import CreateTaskModal from '../components/CreateTaskModal'
|
||||||
|
|
||||||
|
vi.mock('../api/client', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/tasks', () => ({
|
||||||
|
tasksApi: {
|
||||||
|
create: vi.fn(),
|
||||||
|
list: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { tasksApi } from '../api/tasks'
|
||||||
|
|
||||||
|
const renderModal = (props = {}) => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<CreateTaskModal open={true} onClose={vi.fn()} {...props} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CreateTaskModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render when open=false', () => {
|
||||||
|
const qc = new QueryClient()
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<CreateTaskModal open={false} onClose={vi.fn()} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
expect(screen.queryByText('Новая задача')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders form when open=true', () => {
|
||||||
|
renderModal()
|
||||||
|
expect(screen.getByText('Новая задача')).toBeInTheDocument()
|
||||||
|
// Actual placeholder in component
|
||||||
|
expect(screen.getByPlaceholderText('Что нужно сделать?')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error when submitting empty title', async () => {
|
||||||
|
renderModal()
|
||||||
|
fireEvent.click(screen.getByText('Создать задачу'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Введи название задачи')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits task successfully', async () => {
|
||||||
|
tasksApi.create.mockResolvedValueOnce({ id: 1, title: 'Test Task' })
|
||||||
|
const onClose = vi.fn()
|
||||||
|
renderModal({ onClose })
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Что нужно сделать?'), {
|
||||||
|
target: { value: 'Test Task' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Создать задачу'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.create).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders priority buttons', () => {
|
||||||
|
renderModal()
|
||||||
|
expect(screen.getByText('Без приоритета')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Низкий')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Средний')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Высокий')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders color picker', () => {
|
||||||
|
renderModal()
|
||||||
|
// Colors are rendered as buttons/divs
|
||||||
|
const colorElements = document.querySelectorAll('[style*="background"]')
|
||||||
|
expect(colorElements.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onClose when X clicked', () => {
|
||||||
|
const onClose = vi.fn()
|
||||||
|
renderModal({ onClose })
|
||||||
|
const closeBtn = screen.getAllByRole('button')[0]
|
||||||
|
fireEvent.click(closeBtn)
|
||||||
|
expect(onClose).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders recurring toggle', () => {
|
||||||
|
renderModal()
|
||||||
|
// The label says "Повторять" in the component
|
||||||
|
expect(screen.getByText('Повторять')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
124
src/__tests__/EditHabitModal.test.jsx
Normal file
124
src/__tests__/EditHabitModal.test.jsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import EditHabitModal from '../components/EditHabitModal'
|
||||||
|
|
||||||
|
vi.mock('../api/client', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/habits', () => ({
|
||||||
|
habitsApi: {
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
getFreezes: vi.fn().mockResolvedValue([]),
|
||||||
|
addFreeze: vi.fn(),
|
||||||
|
deleteFreeze: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { habitsApi } from '../api/habits'
|
||||||
|
|
||||||
|
const mockHabit = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Exercise',
|
||||||
|
description: 'Daily workout',
|
||||||
|
color: '#6366f1',
|
||||||
|
icon: '💪',
|
||||||
|
frequency: 'daily',
|
||||||
|
target_days: [],
|
||||||
|
target_count: 1,
|
||||||
|
reminder_time: null,
|
||||||
|
start_date: '2026-01-01',
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderModal = (props = {}) => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<EditHabitModal open={true} onClose={vi.fn()} habit={mockHabit} {...props} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('EditHabitModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render when open=false', () => {
|
||||||
|
const qc = new QueryClient()
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<EditHabitModal open={false} onClose={vi.fn()} habit={mockHabit} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
expect(screen.queryByText('Редактировать привычку')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with habit data', () => {
|
||||||
|
renderModal()
|
||||||
|
expect(screen.getByText('Редактировать привычку')).toBeInTheDocument()
|
||||||
|
expect(screen.getByDisplayValue('Exercise')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders save button', () => {
|
||||||
|
renderModal()
|
||||||
|
// Button text is "Сохранить изменения"
|
||||||
|
expect(screen.getByText('Сохранить изменения')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits updated habit', async () => {
|
||||||
|
habitsApi.update.mockResolvedValueOnce({ id: 1, name: 'Updated' })
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByDisplayValue('Exercise'), {
|
||||||
|
target: { value: 'Updated Exercise' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Сохранить изменения'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(habitsApi.update).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders delete button', () => {
|
||||||
|
renderModal()
|
||||||
|
// Button text is "Удалить привычку"
|
||||||
|
expect(screen.getByText('Удалить привычку')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows delete confirmation when delete clicked', () => {
|
||||||
|
renderModal()
|
||||||
|
fireEvent.click(screen.getByText('Удалить привычку'))
|
||||||
|
// Confirmation shows "Удалить привычку?" and confirm button "Удалить"
|
||||||
|
expect(screen.getByText('Удалить привычку?')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes habit on confirmation', async () => {
|
||||||
|
habitsApi.delete.mockResolvedValueOnce({})
|
||||||
|
renderModal()
|
||||||
|
fireEvent.click(screen.getByText('Удалить привычку'))
|
||||||
|
// Confirm button shows "Удалить"
|
||||||
|
const deleteBtn = screen.getAllByText('Удалить').find(el => el.tagName === 'BUTTON')
|
||||||
|
fireEvent.click(deleteBtn)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(habitsApi.delete).toHaveBeenCalledWith(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders freezes section', async () => {
|
||||||
|
renderModal()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Заморозки/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
111
src/__tests__/EditTaskModal.test.jsx
Normal file
111
src/__tests__/EditTaskModal.test.jsx
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import EditTaskModal from '../components/EditTaskModal'
|
||||||
|
|
||||||
|
vi.mock('../api/client', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
put: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/tasks', () => ({
|
||||||
|
tasksApi: {
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { tasksApi } from '../api/tasks'
|
||||||
|
|
||||||
|
const mockTask = {
|
||||||
|
id: 1,
|
||||||
|
title: 'Test Task',
|
||||||
|
description: 'Description',
|
||||||
|
color: '#6366f1',
|
||||||
|
icon: '📋',
|
||||||
|
due_date: '2026-03-26',
|
||||||
|
priority: 1,
|
||||||
|
reminder_time: null,
|
||||||
|
is_recurring: false,
|
||||||
|
recurrence_type: null,
|
||||||
|
recurrence_interval: 1,
|
||||||
|
recurrence_end_date: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderModal = (props = {}) => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<EditTaskModal open={true} onClose={vi.fn()} task={mockTask} {...props} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('EditTaskModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render when open=false', () => {
|
||||||
|
const qc = new QueryClient()
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<EditTaskModal open={false} onClose={vi.fn()} task={mockTask} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
expect(screen.queryByText('Редактировать задачу')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with task data pre-filled', () => {
|
||||||
|
renderModal()
|
||||||
|
const titleInput = screen.getByDisplayValue('Test Task')
|
||||||
|
expect(titleInput).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders edit modal title', () => {
|
||||||
|
renderModal()
|
||||||
|
expect(screen.getByText('Редактировать задачу')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits updated task', async () => {
|
||||||
|
tasksApi.update.mockResolvedValueOnce({ id: 1, title: 'Updated' })
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
const titleInput = screen.getByDisplayValue('Test Task')
|
||||||
|
fireEvent.change(titleInput, { target: { value: 'Updated Task' } })
|
||||||
|
fireEvent.click(screen.getByText('Сохранить'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.update).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows delete confirmation button', () => {
|
||||||
|
renderModal()
|
||||||
|
// Button says "Удалить задачу"
|
||||||
|
expect(screen.getByText('Удалить задачу')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows delete confirmation when delete clicked', () => {
|
||||||
|
renderModal()
|
||||||
|
fireEvent.click(screen.getByText('Удалить задачу'))
|
||||||
|
// Confirmation shows "Да, удалить"
|
||||||
|
expect(screen.getByText('Да, удалить')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('deletes task after confirmation', async () => {
|
||||||
|
tasksApi.delete.mockResolvedValueOnce({})
|
||||||
|
renderModal()
|
||||||
|
fireEvent.click(screen.getByText('Удалить задачу'))
|
||||||
|
fireEvent.click(screen.getByText('Да, удалить'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.delete).toHaveBeenCalledWith(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
93
src/__tests__/Finance.test.jsx
Normal file
93
src/__tests__/Finance.test.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Finance from '../pages/Finance'
|
||||||
|
|
||||||
|
vi.mock('../components/Navigation', () => ({
|
||||||
|
default: () => <nav data-testid="navigation">Nav</nav>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/finance/FinanceDashboard', () => ({
|
||||||
|
default: ({ month, year }) => <div data-testid="finance-dashboard">Dashboard {month}/{year}</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/finance/TransactionList', () => ({
|
||||||
|
default: ({ onAdd }) => (
|
||||||
|
<div data-testid="transaction-list">
|
||||||
|
<button onClick={onAdd}>Add</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/finance/FinanceAnalytics', () => ({
|
||||||
|
default: () => <div data-testid="finance-analytics">Analytics</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/finance/CategoriesManager', () => ({
|
||||||
|
default: () => <div data-testid="categories-manager">Categories</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/finance/AddTransactionModal', () => ({
|
||||||
|
default: ({ onClose, onSaved }) => (
|
||||||
|
<div data-testid="add-transaction-modal">
|
||||||
|
<button onClick={onClose}>Close</button>
|
||||||
|
<button onClick={onSaved}>Save</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Finance page', () => {
|
||||||
|
it('renders finance page header', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
expect(screen.getByText('💰 Финансы')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders tab navigation', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
// Buttons contain emoji + text, use regex
|
||||||
|
expect(screen.getByText(/Обзор/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/Транзакции/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/Аналитика/)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText(/Категории/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows dashboard tab by default', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
expect(screen.getByTestId('finance-dashboard')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches to transactions tab', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
fireEvent.click(screen.getByText(/Транзакции/))
|
||||||
|
expect(screen.getByTestId('transaction-list')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches to analytics tab', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
fireEvent.click(screen.getByText(/Аналитика/))
|
||||||
|
expect(screen.getByTestId('finance-analytics')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches to categories tab', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
fireEvent.click(screen.getByText(/Категории/))
|
||||||
|
expect(screen.getByTestId('categories-manager')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders navigation', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
expect(screen.getByTestId('navigation')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can navigate months', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
expect(screen.getByTestId('finance-dashboard')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens add transaction modal from transactions tab', () => {
|
||||||
|
render(<MemoryRouter><Finance /></MemoryRouter>)
|
||||||
|
fireEvent.click(screen.getByText(/Транзакции/))
|
||||||
|
fireEvent.click(screen.getByText('Add'))
|
||||||
|
expect(screen.getByTestId('add-transaction-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
72
src/__tests__/FinanceDashboard.test.jsx
Normal file
72
src/__tests__/FinanceDashboard.test.jsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import FinanceDashboard from '../components/finance/FinanceDashboard'
|
||||||
|
|
||||||
|
vi.mock('recharts', () => ({
|
||||||
|
PieChart: ({ children }) => <div data-testid="pie-chart">{children}</div>,
|
||||||
|
Pie: () => <div />,
|
||||||
|
Cell: () => <div />,
|
||||||
|
LineChart: ({ children }) => <div data-testid="line-chart">{children}</div>,
|
||||||
|
Line: () => <div />,
|
||||||
|
XAxis: () => <div />,
|
||||||
|
YAxis: () => <div />,
|
||||||
|
Tooltip: () => <div />,
|
||||||
|
ResponsiveContainer: ({ children }) => <div>{children}</div>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/finance', () => ({
|
||||||
|
financeApi: {
|
||||||
|
getSummary: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { financeApi } from '../api/finance'
|
||||||
|
|
||||||
|
describe('FinanceDashboard', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows loading state', () => {
|
||||||
|
financeApi.getSummary.mockReturnValue(new Promise(() => {}))
|
||||||
|
render(<FinanceDashboard month={3} year={2026} />)
|
||||||
|
const skeletons = document.querySelectorAll('.animate-pulse')
|
||||||
|
expect(skeletons.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty state when no data', async () => {
|
||||||
|
financeApi.getSummary.mockResolvedValueOnce({
|
||||||
|
total_income: 0,
|
||||||
|
total_expense: 0,
|
||||||
|
balance: 0,
|
||||||
|
carried_over: 0,
|
||||||
|
by_category: [],
|
||||||
|
daily: [],
|
||||||
|
})
|
||||||
|
render(<FinanceDashboard month={3} year={2026} />)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Нет данных')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows dashboard when data available', async () => {
|
||||||
|
financeApi.getSummary.mockResolvedValueOnce({
|
||||||
|
total_income: 100000,
|
||||||
|
total_expense: 50000,
|
||||||
|
balance: 50000,
|
||||||
|
carried_over: 0,
|
||||||
|
by_category: [
|
||||||
|
{ category_name: 'Еда', category_emoji: '🍔', type: 'expense', amount: 10000 },
|
||||||
|
],
|
||||||
|
daily: [
|
||||||
|
{ date: '2026-03-01', income: 0, expense: 500 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
render(<FinanceDashboard month={3} year={2026} />)
|
||||||
|
await waitFor(() => {
|
||||||
|
// Use getAllByText since "50 000" appears multiple times
|
||||||
|
const elements = screen.getAllByText(/50\s*000/)
|
||||||
|
expect(elements.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
107
src/__tests__/ForgotPassword.test.jsx
Normal file
107
src/__tests__/ForgotPassword.test.jsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import ForgotPassword from '../pages/ForgotPassword'
|
||||||
|
|
||||||
|
vi.mock('../api/client', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
interceptors: {
|
||||||
|
request: { use: vi.fn() },
|
||||||
|
response: { use: vi.fn() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import api from '../api/client'
|
||||||
|
|
||||||
|
describe('ForgotPassword page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderPage = () => render(<MemoryRouter><ForgotPassword /></MemoryRouter>)
|
||||||
|
|
||||||
|
it('renders form', () => {
|
||||||
|
renderPage()
|
||||||
|
expect(screen.getByText('Забыли пароль?')).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText('your@email.com')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Отправить ссылку')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders back to login link', () => {
|
||||||
|
renderPage()
|
||||||
|
expect(screen.getByText('Вернуться ко входу')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows success state after submit', async () => {
|
||||||
|
api.post.mockResolvedValueOnce({})
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'test@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Отправить ссылку'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Письмо отправлено! 📬')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows email in success state', async () => {
|
||||||
|
api.post.mockResolvedValueOnce({})
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'myemail@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Отправить ссылку'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/myemail@test\.com/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error on failure', async () => {
|
||||||
|
api.post.mockRejectedValueOnce({ response: { data: { error: 'Пользователь не найден' } } })
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'bad@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Отправить ссылку'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Пользователь не найден')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows default error message', async () => {
|
||||||
|
api.post.mockRejectedValueOnce(new Error('Network'))
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'bad@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Отправить ссылку'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Ошибка отправки')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls correct API endpoint', async () => {
|
||||||
|
api.post.mockResolvedValueOnce({})
|
||||||
|
renderPage()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'test@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Отправить ссылку'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.post).toHaveBeenCalledWith('/auth/forgot-password', { email: 'test@test.com' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
103
src/__tests__/Habits.test.jsx
Normal file
103
src/__tests__/Habits.test.jsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Habits from '../pages/Habits'
|
||||||
|
|
||||||
|
vi.mock('../api/habits', () => ({
|
||||||
|
habitsApi: {
|
||||||
|
list: vi.fn(),
|
||||||
|
getHabitStats: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/CreateHabitModal', () => ({
|
||||||
|
default: ({ open, onClose }) => open ? (
|
||||||
|
<div data-testid="create-habit-modal">
|
||||||
|
<button onClick={onClose}>Close</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/EditHabitModal', () => ({
|
||||||
|
default: ({ open, onClose }) => open ? (
|
||||||
|
<div data-testid="edit-habit-modal">
|
||||||
|
<button onClick={onClose}>Close</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/Navigation', () => ({
|
||||||
|
default: () => <nav data-testid="navigation">Nav</nav>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { habitsApi } from '../api/habits'
|
||||||
|
|
||||||
|
const mockHabits = [
|
||||||
|
{ id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' },
|
||||||
|
{ id: 2, name: 'Read', frequency: 'weekly', target_days: [1,2,3,4,5], color: '#22c55e', icon: '📚', is_archived: false, created_at: '2026-01-01T00:00:00Z' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const renderHabits = (embedded = false) => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<Habits embedded={embedded} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Habits page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
habitsApi.list.mockResolvedValue(mockHabits)
|
||||||
|
habitsApi.getHabitStats.mockResolvedValue({ streak: 5, completion_rate: 80 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders habits list', async () => {
|
||||||
|
renderHabits()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Exercise')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Read')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders header when not embedded', async () => {
|
||||||
|
renderHabits(false)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Мои привычки')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render header when embedded', async () => {
|
||||||
|
renderHabits(true)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Мои привычки')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens create habit modal', async () => {
|
||||||
|
renderHabits()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Мои привычки')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Новая'))
|
||||||
|
expect(screen.getByTestId('create-habit-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders navigation when not embedded', () => {
|
||||||
|
renderHabits(false)
|
||||||
|
expect(screen.getByTestId('navigation')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty state when no habits', async () => {
|
||||||
|
habitsApi.list.mockResolvedValue([])
|
||||||
|
renderHabits()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Нет привычек/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
397
src/__tests__/Home.test.jsx
Normal file
397
src/__tests__/Home.test.jsx
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Home from '../pages/Home'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
vi.mock('../store/auth', () => ({
|
||||||
|
useAuthStore: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/habits', () => ({
|
||||||
|
habitsApi: {
|
||||||
|
list: vi.fn(),
|
||||||
|
getLogs: vi.fn(),
|
||||||
|
log: vi.fn(),
|
||||||
|
getStats: vi.fn(),
|
||||||
|
getHabitStats: vi.fn(),
|
||||||
|
getFreezes: vi.fn(),
|
||||||
|
deleteLog: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/tasks', () => ({
|
||||||
|
tasksApi: {
|
||||||
|
today: vi.fn(),
|
||||||
|
complete: vi.fn(),
|
||||||
|
uncomplete: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/Navigation', () => ({
|
||||||
|
default: () => <nav data-testid="navigation">Nav</nav>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/CreateTaskModal', () => ({
|
||||||
|
default: ({ open, onClose }) => open ? <div data-testid="create-task-modal"><button onClick={onClose}>Close</button></div> : null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/LogHabitModal', () => ({
|
||||||
|
default: ({ open, onClose, habit, onLogDate }) => open ? (
|
||||||
|
<div data-testid="log-habit-modal">
|
||||||
|
<span>{habit?.name}</span>
|
||||||
|
<button onClick={onClose}>Close</button>
|
||||||
|
<button onClick={() => onLogDate && onLogDate(habit?.id, '2026-03-01')}>Log Date</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { habitsApi } from '../api/habits'
|
||||||
|
import { tasksApi } from '../api/tasks'
|
||||||
|
|
||||||
|
const mockUser = { id: 1, username: 'testuser', email: 'test@test.com' }
|
||||||
|
const mockLogout = vi.fn()
|
||||||
|
|
||||||
|
const mockHabits = [
|
||||||
|
{ id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' },
|
||||||
|
{ id: 2, name: 'Read', frequency: 'weekly', target_days: [1,2,3,4,5,6,7], color: '#22c55e', icon: '📚', is_archived: false, created_at: '2026-01-01T00:00:00Z' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockTasks = [
|
||||||
|
{ id: 1, title: 'Buy groceries', completed: false, priority: 1, due_date: null, icon: '📋', color: '#6366f1', is_recurring: false, recurrence_type: null },
|
||||||
|
{ id: 2, title: 'Completed task', completed: true, priority: 0, due_date: null, icon: '✅', color: '#22c55e', is_recurring: false, recurrence_type: null },
|
||||||
|
]
|
||||||
|
|
||||||
|
const renderHome = () => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<Home />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Home page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
useAuthStore.mockReturnValue({ user: mockUser, logout: mockLogout })
|
||||||
|
habitsApi.list.mockResolvedValue([])
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
habitsApi.getStats.mockResolvedValue({ total_habits: 0, completion_rate: 0, today_completed: 3, active_habits: 5 })
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
habitsApi.deleteLog.mockResolvedValue({})
|
||||||
|
tasksApi.today.mockResolvedValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders home page', async () => {
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Привет|Главная/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders navigation', () => {
|
||||||
|
renderHome()
|
||||||
|
expect(screen.getByTestId('navigation')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows user greeting', async () => {
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/testuser/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls logout when logout button clicked', async () => {
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/testuser/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const logoutBtn = document.querySelector('[title="Выйти"]')
|
||||||
|
if (logoutBtn) {
|
||||||
|
fireEvent.click(logoutBtn)
|
||||||
|
expect(mockLogout).toHaveBeenCalled()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows progress section', async () => {
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Прогресс на сегодня')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows stats when available', async () => {
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Выполнено')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Активных')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty tasks state', async () => {
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Нет задач на сегодня')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders tasks when present', async () => {
|
||||||
|
tasksApi.today.mockResolvedValue(mockTasks)
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders only active tasks in main list', async () => {
|
||||||
|
tasksApi.today.mockResolvedValue(mockTasks)
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows free day message when no habits for today', async () => {
|
||||||
|
habitsApi.list.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Свободный день!')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders habits section', async () => {
|
||||||
|
habitsApi.list.mockResolvedValue(mockHabits)
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Привычки')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens create task modal when plus button clicked', async () => {
|
||||||
|
tasksApi.today.mockResolvedValue(mockTasks)
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Задачи на сегодня')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const plusBtns = document.querySelectorAll('button')
|
||||||
|
const plusBtn = Array.from(plusBtns).find(b => b.querySelector('svg'))
|
||||||
|
// Click the + button in tasks header
|
||||||
|
const taskHeader = screen.getByText('Задачи на сегодня')
|
||||||
|
const headerDiv = taskHeader.closest('div')
|
||||||
|
const btnsInHeader = headerDiv?.querySelectorAll('button')
|
||||||
|
if (btnsInHeader && btnsInHeader.length > 0) {
|
||||||
|
fireEvent.click(btnsInHeader[0])
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens create task modal from empty state', async () => {
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('+ Добавить задачу')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('+ Добавить задачу'))
|
||||||
|
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes create task modal', async () => {
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('+ Добавить задачу')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('+ Добавить задачу'))
|
||||||
|
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
|
||||||
|
fireEvent.click(screen.getByText('Close'))
|
||||||
|
expect(screen.queryByTestId('create-task-modal')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles task complete', async () => {
|
||||||
|
tasksApi.complete.mockResolvedValue({ id: 1, completed: true })
|
||||||
|
tasksApi.today.mockResolvedValue([mockTasks[0]])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const buttons = document.querySelectorAll('button')
|
||||||
|
const completeBtn = Array.from(buttons).find(b => b.className && b.className.includes('rounded-xl') && b.querySelector('span'))
|
||||||
|
if (completeBtn) {
|
||||||
|
fireEvent.click(completeBtn)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.complete).toHaveBeenCalledWith(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles uncomplete task - api setup', async () => {
|
||||||
|
tasksApi.uncomplete.mockResolvedValue({ id: 2, completed: false })
|
||||||
|
tasksApi.today.mockResolvedValue([mockTasks[0]])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
// Verify uncomplete api is mocked properly
|
||||||
|
expect(tasksApi.uncomplete).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows habits with daily frequency', async () => {
|
||||||
|
const dailyHabit = { id: 1, name: 'Daily Habit', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
|
||||||
|
habitsApi.list.mockResolvedValue([dailyHabit])
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Daily Habit')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows completion message when all habits done', async () => {
|
||||||
|
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
|
||||||
|
habitsApi.list.mockResolvedValue([habit])
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
habitsApi.getLogs.mockResolvedValue([{ id: 10, date: today }])
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Все привычки выполнены/)).toBeInTheDocument()
|
||||||
|
}, { timeout: 3000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens log habit modal from calendar button', async () => {
|
||||||
|
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
|
||||||
|
habitsApi.list.mockResolvedValue([habit])
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Exercise')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const calendarBtn = document.querySelector('[title="Отметить за другой день"]')
|
||||||
|
if (calendarBtn) {
|
||||||
|
fireEvent.click(calendarBtn)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('log-habit-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes log habit modal', async () => {
|
||||||
|
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
|
||||||
|
habitsApi.list.mockResolvedValue([habit])
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Exercise')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const calendarBtn = document.querySelector('[title="Отметить за другой день"]')
|
||||||
|
if (calendarBtn) {
|
||||||
|
fireEvent.click(calendarBtn)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('log-habit-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Close'))
|
||||||
|
expect(screen.queryByTestId('log-habit-modal')).not.toBeInTheDocument()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles habit toggle (log)', async () => {
|
||||||
|
habitsApi.log.mockResolvedValue({ id: 99, date: new Date().toISOString().split('T')[0] })
|
||||||
|
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
|
||||||
|
habitsApi.list.mockResolvedValue([habit])
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Exercise')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const habitBtns = document.querySelectorAll('button.rounded-2xl')
|
||||||
|
if (habitBtns.length > 0) {
|
||||||
|
fireEvent.click(habitBtns[0])
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(habitsApi.log).toHaveBeenCalledWith(1, {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles habit toggle (delete log when already done)', async () => {
|
||||||
|
habitsApi.deleteLog.mockResolvedValue({})
|
||||||
|
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
|
||||||
|
habitsApi.list.mockResolvedValue([habit])
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
habitsApi.getLogs.mockResolvedValue([{ id: 5, date: today }])
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Exercise')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
// After logs load, find the undo button for habit
|
||||||
|
await waitFor(() => {
|
||||||
|
const undoBtns = document.querySelectorAll('[title="Отменить"]')
|
||||||
|
expect(undoBtns.length).toBeGreaterThan(0)
|
||||||
|
}, { timeout: 3000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders tasks section header', async () => {
|
||||||
|
tasksApi.today.mockResolvedValue([mockTasks[0]])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Задачи на сегодня')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles log date from modal', async () => {
|
||||||
|
habitsApi.log.mockResolvedValue({ id: 99, date: '2026-03-01' })
|
||||||
|
const habit = { id: 1, name: 'Exercise', frequency: 'daily', color: '#6366f1', icon: '💪', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
|
||||||
|
habitsApi.list.mockResolvedValue([habit])
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Exercise')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const calendarBtn = document.querySelector('[title="Отметить за другой день"]')
|
||||||
|
if (calendarBtn) {
|
||||||
|
fireEvent.click(calendarBtn)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('log-habit-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Log Date'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(habitsApi.log).toHaveBeenCalledWith(1, { date: '2026-03-01' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test helper functions directly
|
||||||
|
describe('shouldShowToday helper', () => {
|
||||||
|
it('renders frozen habit indicator when habit is frozen', async () => {
|
||||||
|
const habit = { id: 1, name: 'Frozen Habit', frequency: 'daily', color: '#6366f1', icon: '❄️', is_archived: false, created_at: '2026-01-01T00:00:00Z' }
|
||||||
|
habitsApi.list.mockResolvedValue([habit])
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
const today = new Date().toISOString().split('T')[0]
|
||||||
|
// Create a freeze that covers today
|
||||||
|
const yesterday = new Date()
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
|
const tomorrow = new Date()
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||||
|
habitsApi.getFreezes.mockResolvedValue([{
|
||||||
|
start_date: yesterday.toISOString().split('T')[0],
|
||||||
|
end_date: tomorrow.toISOString().split('T')[0]
|
||||||
|
}])
|
||||||
|
renderHome()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/паузе/)).toBeInTheDocument()
|
||||||
|
}, { timeout: 3000 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
102
src/__tests__/LogHabitModal.test.jsx
Normal file
102
src/__tests__/LogHabitModal.test.jsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import LogHabitModal from '../components/LogHabitModal'
|
||||||
|
|
||||||
|
describe('LogHabitModal', () => {
|
||||||
|
const mockHabit = { id: 1, name: 'Exercise', color: '#6366f1', icon: '🏃' }
|
||||||
|
const mockOnClose = vi.fn()
|
||||||
|
const mockOnLogDate = vi.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render when open=false', () => {
|
||||||
|
render(
|
||||||
|
<LogHabitModal
|
||||||
|
open={false}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
habit={mockHabit}
|
||||||
|
completedDates={[]}
|
||||||
|
onLogDate={mockOnLogDate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(screen.queryByText('Exercise')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders modal when open=true', () => {
|
||||||
|
render(
|
||||||
|
<LogHabitModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
habit={mockHabit}
|
||||||
|
completedDates={[]}
|
||||||
|
onLogDate={mockOnLogDate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(screen.getByText('Exercise')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders calendar', () => {
|
||||||
|
render(
|
||||||
|
<LogHabitModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
habit={mockHabit}
|
||||||
|
completedDates={[]}
|
||||||
|
onLogDate={mockOnLogDate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
// Calendar days should be present
|
||||||
|
const dayButtons = screen.getAllByRole('button')
|
||||||
|
expect(dayButtons.length).toBeGreaterThan(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders prev/next month navigation', () => {
|
||||||
|
render(
|
||||||
|
<LogHabitModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
habit={mockHabit}
|
||||||
|
completedDates={[]}
|
||||||
|
onLogDate={mockOnLogDate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
// Check navigation arrows
|
||||||
|
const buttons = screen.getAllByRole('button')
|
||||||
|
expect(buttons.length).toBeGreaterThan(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onClose when backdrop clicked', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<LogHabitModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
habit={mockHabit}
|
||||||
|
completedDates={[]}
|
||||||
|
onLogDate={mockOnLogDate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
// Click backdrop (first child overlay)
|
||||||
|
const backdrop = container.querySelector('.fixed.inset-0')
|
||||||
|
fireEvent.click(backdrop)
|
||||||
|
expect(mockOnClose).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls onClose when X button clicked', () => {
|
||||||
|
render(
|
||||||
|
<LogHabitModal
|
||||||
|
open={true}
|
||||||
|
onClose={mockOnClose}
|
||||||
|
habit={mockHabit}
|
||||||
|
completedDates={[]}
|
||||||
|
onLogDate={mockOnLogDate}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
// Find close button (X icon)
|
||||||
|
const closeBtn = screen.getAllByRole('button')[0]
|
||||||
|
fireEvent.click(closeBtn)
|
||||||
|
// Some button should trigger close
|
||||||
|
expect(mockOnClose).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
119
src/__tests__/Login.test.jsx
Normal file
119
src/__tests__/Login.test.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Login from '../pages/Login'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
vi.mock('../store/auth', () => ({
|
||||||
|
useAuthStore: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockNavigate = vi.fn()
|
||||||
|
vi.mock('react-router-dom', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Login page', () => {
|
||||||
|
const mockLogin = vi.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
useAuthStore.mockImplementation((selector) =>
|
||||||
|
selector({ login: mockLogin })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderLogin = () => render(<MemoryRouter><Login /></MemoryRouter>)
|
||||||
|
|
||||||
|
it('renders login form', () => {
|
||||||
|
renderLogin()
|
||||||
|
expect(screen.getByPlaceholderText('your@email.com')).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText('••••••••')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Войти')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders "С возвращением!" heading', () => {
|
||||||
|
renderLogin()
|
||||||
|
expect(screen.getByText('С возвращением!')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders forgot password link', () => {
|
||||||
|
renderLogin()
|
||||||
|
expect(screen.getByText('Забыли пароль?')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders register link', () => {
|
||||||
|
renderLogin()
|
||||||
|
expect(screen.getByText('Зарегистрируйся')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits login form successfully', async () => {
|
||||||
|
mockLogin.mockResolvedValueOnce({})
|
||||||
|
renderLogin()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'test@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('••••••••'), {
|
||||||
|
target: { value: 'password123' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Войти'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockLogin).toHaveBeenCalledWith('test@test.com', 'password123')
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error on login failure', async () => {
|
||||||
|
mockLogin.mockRejectedValueOnce({ response: { data: { error: 'Неверный пароль' } } })
|
||||||
|
renderLogin()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'test@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('••••••••'), {
|
||||||
|
target: { value: 'wrongpass' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Войти'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Неверный пароль')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows default error message on login failure', async () => {
|
||||||
|
mockLogin.mockRejectedValueOnce(new Error('Network error'))
|
||||||
|
renderLogin()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'test@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('••••••••'), {
|
||||||
|
target: { value: 'pass' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Войти'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Ошибка входа')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles password visibility', () => {
|
||||||
|
renderLogin()
|
||||||
|
const passwordInput = screen.getByPlaceholderText('••••••••')
|
||||||
|
expect(passwordInput.type).toBe('password')
|
||||||
|
|
||||||
|
// Find the toggle button (Eye icon button)
|
||||||
|
const toggleBtn = passwordInput.parentElement.querySelector('button[type="button"]')
|
||||||
|
fireEvent.click(toggleBtn)
|
||||||
|
expect(passwordInput.type).toBe('text')
|
||||||
|
|
||||||
|
fireEvent.click(toggleBtn)
|
||||||
|
expect(passwordInput.type).toBe('password')
|
||||||
|
})
|
||||||
|
})
|
||||||
53
src/__tests__/Navigation.test.jsx
Normal file
53
src/__tests__/Navigation.test.jsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Navigation from '../components/Navigation'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
vi.mock('../store/auth', () => ({
|
||||||
|
useAuthStore: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('Navigation component', () => {
|
||||||
|
const renderNav = (user = null, path = '/') =>
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={[path]}>
|
||||||
|
<Navigation />
|
||||||
|
</MemoryRouter>
|
||||||
|
)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
useAuthStore.mockImplementation((selector) => selector({ user: null }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders navigation with main links', () => {
|
||||||
|
renderNav()
|
||||||
|
expect(screen.getByText('Главная')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Трекер')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Накопления')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Настройки')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders navigation as nav element', () => {
|
||||||
|
renderNav()
|
||||||
|
expect(screen.getByRole('navigation')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders links for all nav items', () => {
|
||||||
|
renderNav()
|
||||||
|
const links = screen.getAllByRole('link')
|
||||||
|
expect(links.length).toBeGreaterThanOrEqual(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with user as owner', () => {
|
||||||
|
useAuthStore.mockImplementation((selector) => selector({ user: { id: 1 } }))
|
||||||
|
renderNav()
|
||||||
|
expect(screen.getByText('Главная')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders with non-owner user', () => {
|
||||||
|
useAuthStore.mockImplementation((selector) => selector({ user: { id: 2 } }))
|
||||||
|
renderNav()
|
||||||
|
expect(screen.getByText('Главная')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
103
src/__tests__/Register.test.jsx
Normal file
103
src/__tests__/Register.test.jsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Register from '../pages/Register'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
vi.mock('../store/auth', () => ({
|
||||||
|
useAuthStore: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockNavigate = vi.fn()
|
||||||
|
vi.mock('react-router-dom', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Register page', () => {
|
||||||
|
const mockRegister = vi.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
useAuthStore.mockImplementation((selector) =>
|
||||||
|
selector({ register: mockRegister })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderRegister = () => render(<MemoryRouter><Register /></MemoryRouter>)
|
||||||
|
|
||||||
|
it('renders register form', () => {
|
||||||
|
renderRegister()
|
||||||
|
expect(screen.getByText('Создай аккаунт')).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText('Имя')).toBeInTheDocument()
|
||||||
|
expect(screen.getByPlaceholderText('your@email.com')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Создать аккаунт')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders login link', () => {
|
||||||
|
renderRegister()
|
||||||
|
expect(screen.getByText('Войти')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('submits registration successfully', async () => {
|
||||||
|
mockRegister.mockResolvedValueOnce({})
|
||||||
|
renderRegister()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Имя'), {
|
||||||
|
target: { value: 'TestUser' },
|
||||||
|
})
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), {
|
||||||
|
target: { value: 'test@test.com' },
|
||||||
|
})
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
|
||||||
|
target: { value: 'password123' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Создать аккаунт'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRegister).toHaveBeenCalledWith('test@test.com', 'TestUser', 'password123')
|
||||||
|
expect(mockNavigate).toHaveBeenCalledWith('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error on registration failure', async () => {
|
||||||
|
mockRegister.mockRejectedValueOnce({ response: { data: { error: 'Email already used' } } })
|
||||||
|
renderRegister()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Имя'), { target: { value: 'TestUser' } })
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), { target: { value: 'test@test.com' } })
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), { target: { value: 'password123' } })
|
||||||
|
fireEvent.click(screen.getByText('Создать аккаунт'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Email already used')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows default error on failure', async () => {
|
||||||
|
mockRegister.mockRejectedValueOnce(new Error('Error'))
|
||||||
|
renderRegister()
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Имя'), { target: { value: 'User' } })
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('your@email.com'), { target: { value: 'a@b.com' } })
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), { target: { value: 'password1' } })
|
||||||
|
fireEvent.click(screen.getByText('Создать аккаунт'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Ошибка регистрации')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles password visibility', () => {
|
||||||
|
renderRegister()
|
||||||
|
const passwordInput = screen.getByPlaceholderText('Минимум 8 символов')
|
||||||
|
expect(passwordInput.type).toBe('password')
|
||||||
|
|
||||||
|
const toggleBtn = passwordInput.parentElement.querySelector('button[type="button"]')
|
||||||
|
fireEvent.click(toggleBtn)
|
||||||
|
expect(passwordInput.type).toBe('text')
|
||||||
|
})
|
||||||
|
})
|
||||||
116
src/__tests__/ResetPassword.test.jsx
Normal file
116
src/__tests__/ResetPassword.test.jsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { MemoryRouter, Route, Routes } from 'react-router-dom'
|
||||||
|
import ResetPassword from '../pages/ResetPassword'
|
||||||
|
|
||||||
|
vi.mock('../api/client', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
interceptors: {
|
||||||
|
request: { use: vi.fn() },
|
||||||
|
response: { use: vi.fn() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import api from '../api/client'
|
||||||
|
|
||||||
|
const mockNavigate = vi.fn()
|
||||||
|
vi.mock('react-router-dom', async (importOriginal) => {
|
||||||
|
const actual = await importOriginal()
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ResetPassword page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderPage = (search = '') =>
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={[`/reset-password${search}`]}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
)
|
||||||
|
|
||||||
|
it('renders form', () => {
|
||||||
|
renderPage('?token=abc')
|
||||||
|
// Use getAllByText since "Новый пароль" appears as h1 and label
|
||||||
|
expect(screen.getAllByText('Новый пароль').length).toBeGreaterThan(0)
|
||||||
|
expect(screen.getByPlaceholderText('Минимум 8 символов')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Сохранить пароль')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error when no token on submit', async () => {
|
||||||
|
renderPage()
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
|
||||||
|
target: { value: 'newpassword123' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Сохранить пароль'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Токен не найден')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows success state on successful reset', async () => {
|
||||||
|
api.post.mockResolvedValueOnce({})
|
||||||
|
renderPage('?token=valid-token')
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
|
||||||
|
target: { value: 'newpassword123' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Сохранить пароль'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Пароль изменён! 🎉')).toBeInTheDocument()
|
||||||
|
}, { timeout: 3000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error on failure', async () => {
|
||||||
|
api.post.mockRejectedValueOnce({ response: { data: { error: 'Token invalid' } } })
|
||||||
|
renderPage('?token=bad-token')
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
|
||||||
|
target: { value: 'newpassword123' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Сохранить пароль'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Token invalid')).toBeInTheDocument()
|
||||||
|
}, { timeout: 3000 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles password visibility', () => {
|
||||||
|
renderPage('?token=abc')
|
||||||
|
const passwordInput = screen.getByPlaceholderText('Минимум 8 символов')
|
||||||
|
expect(passwordInput.type).toBe('password')
|
||||||
|
|
||||||
|
const toggleBtn = passwordInput.parentElement.querySelector('button[type="button"]')
|
||||||
|
fireEvent.click(toggleBtn)
|
||||||
|
expect(passwordInput.type).toBe('text')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls correct API endpoint', async () => {
|
||||||
|
api.post.mockResolvedValueOnce({})
|
||||||
|
renderPage('?token=mytoken')
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText('Минимум 8 символов'), {
|
||||||
|
target: { value: 'mynewpassword' },
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Сохранить пароль'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.post).toHaveBeenCalledWith('/auth/reset-password', {
|
||||||
|
token: 'mytoken',
|
||||||
|
new_password: 'mynewpassword',
|
||||||
|
})
|
||||||
|
}, { timeout: 3000 })
|
||||||
|
})
|
||||||
|
})
|
||||||
91
src/__tests__/Savings.test.jsx
Normal file
91
src/__tests__/Savings.test.jsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Savings from '../pages/Savings'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
vi.mock('../store/auth', () => ({
|
||||||
|
useAuthStore: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/savings', () => ({
|
||||||
|
savingsApi: {
|
||||||
|
listCategories: vi.fn(),
|
||||||
|
getStats: vi.fn(),
|
||||||
|
listTransactions: vi.fn(),
|
||||||
|
createCategory: vi.fn(),
|
||||||
|
updateCategory: vi.fn(),
|
||||||
|
deleteCategory: vi.fn(),
|
||||||
|
createTransaction: vi.fn(),
|
||||||
|
deleteTransaction: vi.fn(),
|
||||||
|
getMembers: vi.fn(),
|
||||||
|
getRecurringPlans: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/Navigation', () => ({
|
||||||
|
default: () => <nav data-testid="navigation">Nav</nav>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { savingsApi } from '../api/savings'
|
||||||
|
|
||||||
|
const mockCategories = [
|
||||||
|
{ id: 1, name: 'Квартира', target_amount: 500000, current_amount: 100000, color: '#6366f1', emoji: '🏠', is_shared: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockStats = {
|
||||||
|
total_balance: 100000,
|
||||||
|
categories_count: 1,
|
||||||
|
total_deposited: 150000,
|
||||||
|
total_withdrawn: 50000,
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSavings = () => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<Savings />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Savings page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
useAuthStore.mockImplementation((selector) => selector({ user: { id: 1 } }))
|
||||||
|
savingsApi.listCategories.mockResolvedValue(mockCategories)
|
||||||
|
savingsApi.getStats.mockResolvedValue(mockStats)
|
||||||
|
savingsApi.listTransactions.mockResolvedValue([])
|
||||||
|
savingsApi.getMembers.mockResolvedValue([])
|
||||||
|
savingsApi.getRecurringPlans.mockResolvedValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders savings page', async () => {
|
||||||
|
renderSavings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Накопления')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders navigation', () => {
|
||||||
|
renderSavings()
|
||||||
|
expect(screen.getByTestId('navigation')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders categories', async () => {
|
||||||
|
renderSavings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Квартира')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders tab navigation', async () => {
|
||||||
|
renderSavings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Обзор')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
119
src/__tests__/Settings.test.jsx
Normal file
119
src/__tests__/Settings.test.jsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Settings from '../pages/Settings'
|
||||||
|
import { ThemeProvider } from '../contexts/ThemeContext'
|
||||||
|
|
||||||
|
vi.mock('../api/profile', () => ({
|
||||||
|
profileApi: {
|
||||||
|
get: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/Navigation', () => ({
|
||||||
|
default: () => <nav data-testid="navigation">Nav</nav>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { profileApi } from '../api/profile'
|
||||||
|
|
||||||
|
const mockProfile = {
|
||||||
|
username: 'testuser',
|
||||||
|
telegram_chat_id: 123456,
|
||||||
|
notifications_enabled: true,
|
||||||
|
timezone: 'Europe/Moscow',
|
||||||
|
morning_reminder_time: '09:00',
|
||||||
|
evening_reminder_time: '21:00',
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSettings = () => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Settings />
|
||||||
|
</ThemeProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Settings page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
profileApi.get.mockResolvedValue(mockProfile)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows loading state', () => {
|
||||||
|
profileApi.get.mockReturnValue(new Promise(() => {}))
|
||||||
|
renderSettings()
|
||||||
|
expect(document.querySelector('.animate-spin')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders settings page', async () => {
|
||||||
|
renderSettings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Настройки')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders theme section', async () => {
|
||||||
|
renderSettings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Оформление')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders profile section', async () => {
|
||||||
|
renderSettings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Профиль')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('populates username field from profile', async () => {
|
||||||
|
renderSettings()
|
||||||
|
await waitFor(() => {
|
||||||
|
const input = screen.getByDisplayValue('testuser')
|
||||||
|
expect(input).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles theme', async () => {
|
||||||
|
renderSettings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Тёмная тема|Светлая тема/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const themeBtn = screen.getByText(/Тёмная тема|Светлая тема/)
|
||||||
|
fireEvent.click(themeBtn)
|
||||||
|
expect(screen.getByText(/Тёмная тема|Светлая тема/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saves settings', async () => {
|
||||||
|
profileApi.update.mockResolvedValueOnce(mockProfile)
|
||||||
|
renderSettings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByDisplayValue('testuser')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.change(screen.getByDisplayValue('testuser'), {
|
||||||
|
target: { value: 'newusername' },
|
||||||
|
})
|
||||||
|
await waitFor(() => {
|
||||||
|
// Button says "Сохранить изменения" in Settings
|
||||||
|
expect(screen.getByText(/Сохранить/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText(/Сохранить/))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(profileApi.update).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders Telegram section', async () => {
|
||||||
|
renderSettings()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Telegram')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
98
src/__tests__/Stats.test.jsx
Normal file
98
src/__tests__/Stats.test.jsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Stats from '../pages/Stats'
|
||||||
|
|
||||||
|
vi.mock('recharts', () => ({
|
||||||
|
LineChart: ({ children }) => <div data-testid="line-chart">{children}</div>,
|
||||||
|
Line: () => <div />,
|
||||||
|
BarChart: ({ children }) => <div data-testid="bar-chart">{children}</div>,
|
||||||
|
Bar: () => <div />,
|
||||||
|
XAxis: () => <div />,
|
||||||
|
YAxis: () => <div />,
|
||||||
|
Tooltip: () => <div />,
|
||||||
|
ResponsiveContainer: ({ children }) => <div>{children}</div>,
|
||||||
|
Cell: () => <div />,
|
||||||
|
Area: () => <div />,
|
||||||
|
AreaChart: ({ children }) => <div data-testid="area-chart">{children}</div>,
|
||||||
|
CartesianGrid: () => <div />,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../api/habits', () => ({
|
||||||
|
habitsApi: {
|
||||||
|
list: vi.fn(),
|
||||||
|
getStats: vi.fn(),
|
||||||
|
getLogs: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/Navigation', () => ({
|
||||||
|
default: () => <nav data-testid="navigation">Nav</nav>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { habitsApi } from '../api/habits'
|
||||||
|
|
||||||
|
const mockStats = {
|
||||||
|
total_habits: 3,
|
||||||
|
total_logs: 45,
|
||||||
|
current_streak: 7,
|
||||||
|
best_streak: 14,
|
||||||
|
completion_rate: 82,
|
||||||
|
habits: [
|
||||||
|
{ id: 1, name: 'Exercise', completion_rate: 90, streak: 7 },
|
||||||
|
{ id: 2, name: 'Read', completion_rate: 75, streak: 3 },
|
||||||
|
],
|
||||||
|
daily_completions: [
|
||||||
|
{ date: '2026-03-01', count: 2 },
|
||||||
|
{ date: '2026-03-02', count: 3 },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderStats = () => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<Stats />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Stats page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
habitsApi.list.mockResolvedValue([])
|
||||||
|
habitsApi.getStats.mockResolvedValue(mockStats)
|
||||||
|
habitsApi.getLogs.mockResolvedValue([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders stats page', async () => {
|
||||||
|
renderStats()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Статистика')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders navigation', () => {
|
||||||
|
renderStats()
|
||||||
|
expect(screen.getByTestId('navigation')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows habit selector', async () => {
|
||||||
|
habitsApi.list.mockResolvedValue([
|
||||||
|
{ id: 1, name: 'Exercise', color: '#6366f1', icon: '💪' },
|
||||||
|
])
|
||||||
|
renderStats()
|
||||||
|
// Open dropdown to see habit names
|
||||||
|
await waitFor(() => {
|
||||||
|
// Click the habit selector button to open dropdown
|
||||||
|
const selectorBtn = document.querySelector('button.w-full')
|
||||||
|
if (selectorBtn) fireEvent.click(selectorBtn)
|
||||||
|
})
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Exercise')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
312
src/__tests__/Tasks.test.jsx
Normal file
312
src/__tests__/Tasks.test.jsx
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { MemoryRouter } from 'react-router-dom'
|
||||||
|
import Tasks from '../pages/Tasks'
|
||||||
|
|
||||||
|
vi.mock('../api/tasks', () => ({
|
||||||
|
tasksApi: {
|
||||||
|
list: vi.fn(),
|
||||||
|
complete: vi.fn(),
|
||||||
|
uncomplete: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/CreateTaskModal', () => ({
|
||||||
|
default: ({ open, onClose }) => open ? (
|
||||||
|
<div data-testid="create-task-modal">
|
||||||
|
<button onClick={onClose}>Close</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/EditTaskModal', () => ({
|
||||||
|
default: ({ open, onClose, task }) => open ? (
|
||||||
|
<div data-testid="edit-task-modal">
|
||||||
|
<span>{task?.title}</span>
|
||||||
|
<button onClick={onClose}>Close</button>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('../components/Navigation', () => ({
|
||||||
|
default: () => <nav data-testid="navigation">Nav</nav>,
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { tasksApi } from '../api/tasks'
|
||||||
|
|
||||||
|
const mockTasks = [
|
||||||
|
{ id: 1, title: 'Buy groceries', completed: false, priority: 1, due_date: null, icon: '📋', color: '#6366f1', is_recurring: false, recurrence_type: null, description: '' },
|
||||||
|
{ id: 2, title: 'Read book', completed: false, priority: 2, due_date: '2026-03-30', icon: '📚', color: '#22c55e', is_recurring: false, recurrence_type: null, description: 'Read 30 pages' },
|
||||||
|
{ id: 3, title: 'Completed task', completed: true, priority: 3, due_date: null, icon: '✅', color: '#f59e0b', is_recurring: true, recurrence_type: 'daily', description: '' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockTasksOverdue = [
|
||||||
|
{ id: 4, title: 'Overdue task', completed: false, priority: 0, due_date: '2020-01-01', icon: '⚠️', color: '#ef4444', is_recurring: false, recurrence_type: null, description: '' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const renderTasks = (embedded = false) => {
|
||||||
|
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
||||||
|
return render(
|
||||||
|
<QueryClientProvider client={qc}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<Tasks embedded={embedded} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Tasks page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
tasksApi.list.mockResolvedValue(mockTasks.filter(t => !t.completed))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders tasks list', async () => {
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Read book')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders header when not embedded', async () => {
|
||||||
|
renderTasks(false)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Задачи')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render header when embedded', async () => {
|
||||||
|
renderTasks(true)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByText('Задачи')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders filter buttons', async () => {
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Активные')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Выполненные')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Все')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders navigation when not embedded', () => {
|
||||||
|
renderTasks(false)
|
||||||
|
expect(screen.getByTestId('navigation')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not render navigation when embedded', () => {
|
||||||
|
renderTasks(true)
|
||||||
|
expect(screen.queryByTestId('navigation')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty state when no tasks', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Нет активных задач/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows add task button in empty state for active filter', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Добавить задачу')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens create task modal', async () => {
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Задачи')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const plusBtn = document.querySelector('button.bg-primary-500') || document.querySelector('button[class*="bg-primary"]')
|
||||||
|
if (plusBtn) {
|
||||||
|
fireEvent.click(plusBtn)
|
||||||
|
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens add task modal from empty state button', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Добавить задачу')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Добавить задачу'))
|
||||||
|
expect(screen.getByTestId('create-task-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches to completed filter', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[2]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Выполненные')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Выполненные'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.list).toHaveBeenCalledWith(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('switches to all filter', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue(mockTasks)
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Все')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Все'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.list).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('completes a task on click', async () => {
|
||||||
|
tasksApi.complete.mockResolvedValue({ id: 1, completed: true })
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[0]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
// Get all buttons: [0]=create(+), [1]=complete-task-btn, [2]=edit-btn
|
||||||
|
const allBtns = screen.getAllByRole('button')
|
||||||
|
if (allBtns.length > 1) {
|
||||||
|
fireEvent.click(allBtns[4])
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.complete).toHaveBeenCalledWith(1)
|
||||||
|
}, { timeout: 3000 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows priority badge for tasks', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[1]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Read book')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
expect(screen.getByText('Средний')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows high priority badge', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[2]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Completed task')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
expect(screen.getByText('Высокий')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows due date for tasks', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[1]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Read book')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
expect(document.querySelector('[class*="text-gray"]')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows overdue indicator', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue(mockTasksOverdue)
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Overdue task')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
expect(document.querySelector('svg')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens edit modal when task title clicked', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[0]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Buy groceries'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('edit-task-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes edit modal', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[0]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Buy groceries')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Buy groceries'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId('edit-task-modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Close'))
|
||||||
|
expect(screen.queryByTestId('edit-task-modal')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows recurring icon for recurring tasks', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[2]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Completed task')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
expect(screen.getByText('🔄')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows recurrence label', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[2]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Ежедневно')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows task description', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([mockTasks[1]])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Read 30 pages')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uncompletes a task', async () => {
|
||||||
|
tasksApi.uncomplete.mockResolvedValue({ id: 3, completed: false })
|
||||||
|
const completedTask = { ...mockTasks[2], completed: true }
|
||||||
|
tasksApi.list.mockResolvedValue([completedTask])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Completed task')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
const undoBtn = document.querySelector('[title="Отменить"]')
|
||||||
|
if (undoBtn) {
|
||||||
|
fireEvent.click(undoBtn)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(tasksApi.uncomplete).toHaveBeenCalledWith(3)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty state for completed filter', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Выполненные')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Выполненные'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Нет выполненных задач/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows empty state for all filter', async () => {
|
||||||
|
tasksApi.list.mockResolvedValue([])
|
||||||
|
renderTasks()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Все')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
fireEvent.click(screen.getByText('Все'))
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Нет задач/)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
96
src/__tests__/ThemeContext.test.jsx
Normal file
96
src/__tests__/ThemeContext.test.jsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react'
|
||||||
|
import { ThemeProvider, useTheme } from '../contexts/ThemeContext'
|
||||||
|
|
||||||
|
function ThemeConsumer() {
|
||||||
|
const { theme, toggleTheme } = useTheme()
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span data-testid="theme">{theme}</span>
|
||||||
|
<button onClick={toggleTheme}>Toggle</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ThemeContext', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('provides default dark theme', () => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemeConsumer />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
expect(screen.getByTestId('theme').textContent).toBe('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reads theme from localStorage', () => {
|
||||||
|
localStorage.setItem('theme', 'light')
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemeConsumer />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
expect(screen.getByTestId('theme').textContent).toBe('light')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles from dark to light', () => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemeConsumer />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
expect(screen.getByTestId('theme').textContent).toBe('dark')
|
||||||
|
fireEvent.click(screen.getByText('Toggle'))
|
||||||
|
expect(screen.getByTestId('theme').textContent).toBe('light')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles from light to dark', () => {
|
||||||
|
localStorage.setItem('theme', 'light')
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemeConsumer />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
fireEvent.click(screen.getByText('Toggle'))
|
||||||
|
expect(screen.getByTestId('theme').textContent).toBe('dark')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('persists theme to localStorage', () => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemeConsumer />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
fireEvent.click(screen.getByText('Toggle'))
|
||||||
|
expect(localStorage.getItem('theme')).toBe('light')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds dark class to documentElement', () => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemeConsumer />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
expect(document.documentElement.classList.contains('dark')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes dark class when switching to light', () => {
|
||||||
|
render(
|
||||||
|
<ThemeProvider>
|
||||||
|
<ThemeConsumer />
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
fireEvent.click(screen.getByText('Toggle'))
|
||||||
|
expect(document.documentElement.classList.contains('dark')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws when useTheme used outside provider', () => {
|
||||||
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
expect(() => render(<ThemeConsumer />)).toThrow()
|
||||||
|
consoleError.mockRestore()
|
||||||
|
})
|
||||||
|
})
|
||||||
66
src/__tests__/TransactionList.test.jsx
Normal file
66
src/__tests__/TransactionList.test.jsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import TransactionList from '../components/finance/TransactionList'
|
||||||
|
|
||||||
|
vi.mock('../api/finance', () => ({
|
||||||
|
financeApi: {
|
||||||
|
listCategories: vi.fn(),
|
||||||
|
listTransactions: vi.fn(),
|
||||||
|
deleteTransaction: vi.fn(),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { financeApi } from '../api/finance'
|
||||||
|
|
||||||
|
const mockCategories = [
|
||||||
|
{ id: 1, name: 'Еда', type: 'expense', emoji: '🍔' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const mockTransactions = [
|
||||||
|
{ id: 1, amount: 500, type: 'expense', category_id: 1, description: 'Продукты', date: '2026-03-15T00:00:00Z' },
|
||||||
|
{ id: 2, amount: 1000, type: 'income', category_id: null, description: 'Зарплата', date: '2026-03-01T00:00:00Z' },
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('TransactionList', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
financeApi.listCategories.mockResolvedValue(mockCategories)
|
||||||
|
financeApi.listTransactions.mockResolvedValue(mockTransactions)
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderList = () =>
|
||||||
|
render(<TransactionList onAdd={vi.fn()} month={3} year={2026} />)
|
||||||
|
|
||||||
|
it('shows loading state initially', () => {
|
||||||
|
financeApi.listCategories.mockReturnValue(new Promise(() => {}))
|
||||||
|
financeApi.listTransactions.mockReturnValue(new Promise(() => {}))
|
||||||
|
renderList()
|
||||||
|
const loadingDivs = document.querySelectorAll('.animate-pulse')
|
||||||
|
expect(loadingDivs.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows transactions after loading', async () => {
|
||||||
|
renderList()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Продукты')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows income transaction', async () => {
|
||||||
|
renderList()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Зарплата')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders filter buttons', async () => {
|
||||||
|
renderList()
|
||||||
|
await waitFor(() => {
|
||||||
|
// There are two "Все" buttons (type filter and category filter), use getAllByText
|
||||||
|
const allButtons = screen.getAllByText('Все')
|
||||||
|
expect(allButtons.length).toBeGreaterThan(0)
|
||||||
|
expect(screen.getByText('Расходы')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Доходы')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
84
src/__tests__/VerifyEmail.test.jsx
Normal file
84
src/__tests__/VerifyEmail.test.jsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { MemoryRouter, Route, Routes } from 'react-router-dom'
|
||||||
|
import VerifyEmail from '../pages/VerifyEmail'
|
||||||
|
|
||||||
|
vi.mock('../api/client', () => ({
|
||||||
|
default: {
|
||||||
|
post: vi.fn(),
|
||||||
|
get: vi.fn(),
|
||||||
|
interceptors: {
|
||||||
|
request: { use: vi.fn() },
|
||||||
|
response: { use: vi.fn() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import api from '../api/client'
|
||||||
|
|
||||||
|
describe('VerifyEmail page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderPage = (search = '') =>
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={[`/verify-email${search}`]}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>
|
||||||
|
)
|
||||||
|
|
||||||
|
it('shows loading state initially (with token)', () => {
|
||||||
|
api.post.mockImplementation(() => new Promise(() => {}))
|
||||||
|
renderPage('?token=abc123')
|
||||||
|
expect(screen.getByText('Проверяем...')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error when no token', async () => {
|
||||||
|
renderPage()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Ошибка')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Токен не найден')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows success state on successful verification', async () => {
|
||||||
|
api.post.mockResolvedValueOnce({})
|
||||||
|
renderPage('?token=valid-token')
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Готово! 🎉')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Email успешно подтверждён!')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows login link on success', async () => {
|
||||||
|
api.post.mockResolvedValueOnce({})
|
||||||
|
renderPage('?token=valid-token')
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Войти в аккаунт')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows error state on failed verification', async () => {
|
||||||
|
api.post.mockRejectedValueOnce({ response: { data: { error: 'Token expired' } } })
|
||||||
|
renderPage('?token=expired-token')
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Ошибка')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Token expired')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls verify endpoint with token', async () => {
|
||||||
|
api.post.mockResolvedValueOnce({})
|
||||||
|
renderPage('?token=mytoken')
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(api.post).toHaveBeenCalledWith('/auth/verify-email', { token: 'mytoken' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
26
src/__tests__/test-utils.jsx
Normal file
26
src/__tests__/test-utils.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { ThemeProvider } from '../contexts/ThemeContext'
|
||||||
|
|
||||||
|
function AllProviders({ children }) {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
mutations: { retry: false },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<BrowserRouter>
|
||||||
|
<ThemeProvider>{children}</ThemeProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</QueryClientProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderWithProviders(ui, options) {
|
||||||
|
return render(ui, { wrapper: AllProviders, ...options })
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from '@testing-library/react'
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NavLink } from "react-router-dom"
|
import { NavLink } from "react-router-dom"
|
||||||
import { Home, BarChart3, Wallet, PiggyBank, Settings } from "lucide-react"
|
import { Home, BarChart3, PiggyBank, Settings } from "lucide-react"
|
||||||
import { useAuthStore } from "../store/auth"
|
import { useAuthStore } from "../store/auth"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ export default function Navigation() {
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ to: "/", icon: Home, label: "Главная" },
|
{ to: "/", icon: Home, label: "Главная" },
|
||||||
{ to: "/tracker", icon: BarChart3, label: "Трекер" },
|
{ to: "/tracker", icon: BarChart3, label: "Трекер" },
|
||||||
isOwner && { to: "/finance", icon: Wallet, label: "Финансы" },
|
|
||||||
{ to: "/savings", icon: PiggyBank, label: "Накопления" },
|
{ to: "/savings", icon: PiggyBank, label: "Накопления" },
|
||||||
{ to: "/settings", icon: Settings, label: "Настройки" },
|
{ to: "/settings", icon: Settings, label: "Настройки" },
|
||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { format, startOfWeek, differenceInDays, parseISO, isToday, isTomorrow, i
|
|||||||
import { ru } from 'date-fns/locale'
|
import { ru } from 'date-fns/locale'
|
||||||
import { habitsApi } from '../api/habits'
|
import { habitsApi } from '../api/habits'
|
||||||
import { tasksApi } from '../api/tasks'
|
import { tasksApi } from '../api/tasks'
|
||||||
import { financeApi } from '../api/finance'
|
|
||||||
import { useAuthStore } from '../store/auth'
|
import { useAuthStore } from '../store/auth'
|
||||||
import Navigation from '../components/Navigation'
|
import Navigation from '../components/Navigation'
|
||||||
import CreateTaskModal from '../components/CreateTaskModal'
|
import CreateTaskModal from '../components/CreateTaskModal'
|
||||||
@@ -98,10 +97,6 @@ export default function Home() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const { data: financeSummary } = useQuery({
|
|
||||||
queryKey: ["finance-summary"],
|
|
||||||
queryFn: () => financeApi.getSummary(),
|
|
||||||
})
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (habits.length > 0) {
|
if (habits.length > 0) {
|
||||||
loadTodayLogs()
|
loadTodayLogs()
|
||||||
@@ -308,26 +303,6 @@ export default function Home() {
|
|||||||
|
|
||||||
{/* Tasks */}
|
{/* Tasks */}
|
||||||
|
|
||||||
{/* Finance Summary */}
|
|
||||||
{financeSummary && (
|
|
||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="card p-5">
|
|
||||||
<h2 className="font-semibold text-gray-900 dark:text-white mb-3">💰 Баланс</h2>
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-lg font-bold text-green-500">+{(financeSummary.total_income || 0).toLocaleString("ru-RU")} ₽</p>
|
|
||||||
<p className="text-xs text-gray-500">Доходы</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-lg font-bold text-red-500">-{(financeSummary.total_expense || 0).toLocaleString("ru-RU")} ₽</p>
|
|
||||||
<p className="text-xs text-gray-500">Расходы</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className={"text-lg font-bold " + ((financeSummary.balance || 0) >= 0 ? "text-primary-500" : "text-red-500")}>{(financeSummary.balance || 0).toLocaleString("ru-RU")} ₽</p>
|
|
||||||
<p className="text-xs text-gray-500">Баланс</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
{(activeTasks.length > 0 || !tasksLoading) && (
|
{(activeTasks.length > 0 || !tasksLoading) && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user