viberadar 0.3.110 → 0.3.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/dashboard.html +341 -4
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -693,6 +693,53 @@
|
|
|
693
693
|
.tt-fix-btn:hover { background: rgba(255,200,0,0.1); color: var(--yellow); border-color: var(--yellow); }
|
|
694
694
|
.tt-write-btn { border-color: var(--accent); color: var(--accent); }
|
|
695
695
|
.tt-write-btn:hover { background: rgba(88,166,255,0.1); color: var(--accent); border-color: var(--accent); }
|
|
696
|
+
|
|
697
|
+
/* ─── Test Navigator ──────────────────────────────────────────────── */
|
|
698
|
+
.tn-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 10px; margin-bottom: 16px; }
|
|
699
|
+
.tn-stat-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 12px; }
|
|
700
|
+
.tn-stat-val { font-size: 24px; font-weight: 700; line-height: 1; margin-bottom: 2px; }
|
|
701
|
+
.tn-stat-label { font-size: 11px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.4px; }
|
|
702
|
+
.tn-rows { display: flex; flex-direction: column; gap: 2px; contain: content; }
|
|
703
|
+
.tn-row {
|
|
704
|
+
display: flex; align-items: center; gap: 8px;
|
|
705
|
+
padding: 7px 10px; border-radius: 6px; cursor: pointer;
|
|
706
|
+
font-size: 13px; transition: background 0.1s;
|
|
707
|
+
content-visibility: auto; contain-intrinsic-size: 34px;
|
|
708
|
+
}
|
|
709
|
+
.tn-row:hover { background: var(--bg-card); }
|
|
710
|
+
.tn-row.active { background: var(--bg-card); outline: 1px solid var(--accent); }
|
|
711
|
+
.tn-row.has-problem { border-left: 3px solid var(--red); }
|
|
712
|
+
.tn-row-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex-shrink: 1; }
|
|
713
|
+
.tn-row-dir { font-size: 11px; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-left: auto; flex-shrink: 2; min-width: 0; }
|
|
714
|
+
.tn-badges { display: flex; gap: 4px; flex-wrap: wrap; flex-shrink: 0; }
|
|
715
|
+
.tn-feature-badge {
|
|
716
|
+
display: inline-flex; align-items: center; gap: 3px;
|
|
717
|
+
font-size: 10px; padding: 1px 6px; border-radius: 10px;
|
|
718
|
+
background: var(--bg); border: 1px solid var(--border); cursor: pointer;
|
|
719
|
+
}
|
|
720
|
+
.tn-feature-badge:hover { border-color: var(--dim); }
|
|
721
|
+
.tn-problem-badge {
|
|
722
|
+
font-size: 10px; padding: 1px 6px; border-radius: 10px; font-weight: 600;
|
|
723
|
+
}
|
|
724
|
+
.tn-problem-badge.orphan { background: #3a1a1a; color: var(--red); }
|
|
725
|
+
.tn-problem-badge.stale { background: #3a3020; color: var(--yellow); }
|
|
726
|
+
.tn-problem-badge.duplicate { background: #2a2a3a; color: #b388ff; }
|
|
727
|
+
.tn-source-link { font-size: 11px; color: var(--muted); cursor: pointer; flex-shrink: 0; }
|
|
728
|
+
.tn-source-link:hover { color: var(--text); text-decoration: underline; }
|
|
729
|
+
.tn-no-source { font-size: 11px; color: var(--red); font-style: italic; flex-shrink: 0; }
|
|
730
|
+
.tn-test-count { font-size: 10px; padding: 1px 5px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); color: var(--muted); flex-shrink: 0; }
|
|
731
|
+
.tn-sidebar-section { margin-bottom: 14px; }
|
|
732
|
+
.tn-sidebar-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--dim); margin-bottom: 6px; }
|
|
733
|
+
.tn-filter {
|
|
734
|
+
display: flex; align-items: center; gap: 6px;
|
|
735
|
+
padding: 4px 8px; border-radius: 5px; cursor: pointer;
|
|
736
|
+
font-size: 12px; color: var(--muted); transition: background 0.1s, color 0.1s;
|
|
737
|
+
}
|
|
738
|
+
.tn-filter:hover { background: var(--bg-hover); color: var(--text); }
|
|
739
|
+
.tn-filter.active { background: var(--bg-card); color: var(--text); font-weight: 600; }
|
|
740
|
+
.tn-filter-count { margin-left: auto; font-size: 10px; color: var(--dim); }
|
|
741
|
+
.tn-filter-count.red { color: var(--red); }
|
|
742
|
+
.tn-filter-count.yellow { color: var(--yellow); }
|
|
696
743
|
.file-rows { display: flex; flex-direction: column; gap: 2px; contain: content; }
|
|
697
744
|
.file-row {
|
|
698
745
|
display: flex;
|
|
@@ -1362,6 +1409,7 @@
|
|
|
1362
1409
|
<div class="view-tabs" id="viewTabs">
|
|
1363
1410
|
<div class="view-tab" data-view="features">Features</div>
|
|
1364
1411
|
<div class="view-tab" data-view="files">Files</div>
|
|
1412
|
+
<div class="view-tab" data-view="tests">Tests</div>
|
|
1365
1413
|
</div>
|
|
1366
1414
|
<input class="search-input" id="searchInput" type="text" placeholder="Search…" />
|
|
1367
1415
|
<div id="sidebarExtra"></div>
|
|
@@ -1429,6 +1477,9 @@ const FILE_ROWS_INITIAL_LIMIT = 250;
|
|
|
1429
1477
|
const FILE_ROWS_LIMIT_STEP = 250;
|
|
1430
1478
|
let fileRowsRenderKey = '';
|
|
1431
1479
|
let fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1480
|
+
let testNavType = 'all'; // 'all'|'unit'|'integration'|'e2e'
|
|
1481
|
+
let testNavFeature = null; // null | featureKey string
|
|
1482
|
+
let testNavProblem = null; // null|'no-feature'|'no-source'|'duplicate'|'stale'
|
|
1432
1483
|
let e2ePlan = null; // current E2E plan object
|
|
1433
1484
|
let e2ePlanLoading = false;
|
|
1434
1485
|
let obsActiveTab = 'overview'; // active observability tab
|
|
@@ -1447,9 +1498,9 @@ function switchObsTab(tabId) {
|
|
|
1447
1498
|
});
|
|
1448
1499
|
}
|
|
1449
1500
|
const modeStore = {
|
|
1450
|
-
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1451
|
-
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1452
|
-
docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1501
|
+
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
|
|
1502
|
+
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
|
|
1503
|
+
docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
|
|
1453
1504
|
};
|
|
1454
1505
|
|
|
1455
1506
|
function getModeFromPath(pathname = window.location.pathname) {
|
|
@@ -1479,6 +1530,9 @@ function saveModeState(mode) {
|
|
|
1479
1530
|
drillTestType,
|
|
1480
1531
|
activePanelKey,
|
|
1481
1532
|
showOnlyUntestedInFeature,
|
|
1533
|
+
testNavType,
|
|
1534
|
+
testNavFeature,
|
|
1535
|
+
testNavProblem,
|
|
1482
1536
|
};
|
|
1483
1537
|
}
|
|
1484
1538
|
|
|
@@ -1491,6 +1545,9 @@ function restoreModeState(mode) {
|
|
|
1491
1545
|
drillTestType = state.drillTestType;
|
|
1492
1546
|
activePanelKey = state.activePanelKey;
|
|
1493
1547
|
showOnlyUntestedInFeature = !!state.showOnlyUntestedInFeature;
|
|
1548
|
+
testNavType = state.testNavType || 'all';
|
|
1549
|
+
testNavFeature = state.testNavFeature || null;
|
|
1550
|
+
testNavProblem = state.testNavProblem || null;
|
|
1494
1551
|
}
|
|
1495
1552
|
|
|
1496
1553
|
function switchMode(nextMode) {
|
|
@@ -1602,6 +1659,17 @@ function applyHashRoute() {
|
|
|
1602
1659
|
return;
|
|
1603
1660
|
}
|
|
1604
1661
|
const params = new URLSearchParams(hash);
|
|
1662
|
+
|
|
1663
|
+
// tests view hash: #view=tests&type=e2e&problem=stale&feature=auth
|
|
1664
|
+
if (params.get('view') === 'tests') {
|
|
1665
|
+
view = 'tests';
|
|
1666
|
+
testNavType = params.get('type') || 'all';
|
|
1667
|
+
testNavProblem = params.get('problem') || null;
|
|
1668
|
+
testNavFeature = params.get('tfeature') || null;
|
|
1669
|
+
activePanelKey = null;
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1605
1673
|
const featureKey = params.get('feature');
|
|
1606
1674
|
if (!featureKey) return;
|
|
1607
1675
|
const isKnownFeature = Array.isArray(D?.features) && D.features.some(f => f.key === featureKey);
|
|
@@ -2802,6 +2870,7 @@ function renderSidebar() {
|
|
|
2802
2870
|
t.classList.toggle('active', t.dataset.view === view)
|
|
2803
2871
|
);
|
|
2804
2872
|
|
|
2873
|
+
if (view === 'tests') { renderTestNavSidebar(extra); return; }
|
|
2805
2874
|
if (view !== 'files') { extra.innerHTML = ''; return; }
|
|
2806
2875
|
|
|
2807
2876
|
const types = [...new Set(D.modules.map(m => m.type))].sort();
|
|
@@ -2850,6 +2919,8 @@ function renderContent() {
|
|
|
2850
2919
|
if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
|
|
2851
2920
|
else if (drillFeatureKey) renderFeatureDetail(c);
|
|
2852
2921
|
else renderFeatureCards(c);
|
|
2922
|
+
} else if (view === 'tests') {
|
|
2923
|
+
renderTestNavigator(c);
|
|
2853
2924
|
} else {
|
|
2854
2925
|
renderModuleGrid(c);
|
|
2855
2926
|
}
|
|
@@ -3489,7 +3560,7 @@ function renderObsGlobalDetail(c, filterFeatureKey) {
|
|
|
3489
3560
|
</div>
|
|
3490
3561
|
<div class="obs-hint-section">
|
|
3491
3562
|
<div class="obs-hint-section-title">Как использовать</div>
|
|
3492
|
-
<p>Каждая строчка — пропущенное поле и сколько раз оно отсутствует. Раскрой строку, выбери затронутые модули и нажми <strong>«Добавить
|
|
3563
|
+
<p>Каждая строчка — пропущенное поле и сколько раз оно отсутствует. Раскрой строку, выбери затронутые модули и нажми <strong>«Добавить \`поле\`»</strong> — агент добавит поле во все лог-вызовы выбранных модулей.</p>
|
|
3493
3564
|
</div>
|
|
3494
3565
|
</div>
|
|
3495
3566
|
</div>
|
|
@@ -4781,6 +4852,269 @@ function toggleSourceSelection(relPath, checked) {
|
|
|
4781
4852
|
renderContent();
|
|
4782
4853
|
}
|
|
4783
4854
|
|
|
4855
|
+
// ─── Test Navigator ──────────────────────────────────────────────────────────
|
|
4856
|
+
function buildTestNavData() {
|
|
4857
|
+
const allTests = D.modules.filter(m => m.type === 'test');
|
|
4858
|
+
// reverse map: testRelPath → source modules that reference it
|
|
4859
|
+
const testToSources = new Map();
|
|
4860
|
+
// sourceRelPath → test relPaths (for duplicate detection)
|
|
4861
|
+
const sourceToTests = new Map();
|
|
4862
|
+
for (const m of D.modules) {
|
|
4863
|
+
if (m.type === 'test' || !m.testFile) continue;
|
|
4864
|
+
const tf = m.testFile.replace(/\\/g, '/');
|
|
4865
|
+
if (!testToSources.has(tf)) testToSources.set(tf, []);
|
|
4866
|
+
testToSources.get(tf).push(m);
|
|
4867
|
+
const sp = m.relativePath.replace(/\\/g, '/');
|
|
4868
|
+
if (!sourceToTests.has(sp)) sourceToTests.set(sp, []);
|
|
4869
|
+
sourceToTests.get(sp).push(tf);
|
|
4870
|
+
}
|
|
4871
|
+
return allTests.map(t => {
|
|
4872
|
+
const rel = t.relativePath.replace(/\\/g, '/');
|
|
4873
|
+
const linkedSources = testToSources.get(rel) || [];
|
|
4874
|
+
const features = t.featureKeys || [];
|
|
4875
|
+
const noFeature = features.length === 0;
|
|
4876
|
+
const noSource = linkedSources.length === 0;
|
|
4877
|
+
const stale = t.testStale === true;
|
|
4878
|
+
// duplicate: any linked source is also covered by another test of same type
|
|
4879
|
+
const duplicate = linkedSources.some(s => {
|
|
4880
|
+
const sp = s.relativePath.replace(/\\/g, '/');
|
|
4881
|
+
const tests = sourceToTests.get(sp) || [];
|
|
4882
|
+
if (tests.length <= 1) return false;
|
|
4883
|
+
const sameTypeTests = tests.filter(tp => {
|
|
4884
|
+
const tm = allTests.find(x => x.relativePath.replace(/\\/g, '/') === tp);
|
|
4885
|
+
return tm && tm.testType === t.testType;
|
|
4886
|
+
});
|
|
4887
|
+
return sameTypeTests.length > 1;
|
|
4888
|
+
});
|
|
4889
|
+
const problems = [];
|
|
4890
|
+
if (noFeature) problems.push('no-feature');
|
|
4891
|
+
if (noSource) problems.push('no-source');
|
|
4892
|
+
if (duplicate) problems.push('duplicate');
|
|
4893
|
+
if (stale) problems.push('stale');
|
|
4894
|
+
return { module: t, linkedSources, features, problems };
|
|
4895
|
+
});
|
|
4896
|
+
}
|
|
4897
|
+
|
|
4898
|
+
function renderTestNavSidebar(extra) {
|
|
4899
|
+
const data = buildTestNavData();
|
|
4900
|
+
const counts = { all: data.length, unit: 0, integration: 0, e2e: 0 };
|
|
4901
|
+
const problemCounts = { 'no-feature': 0, 'no-source': 0, duplicate: 0, stale: 0 };
|
|
4902
|
+
for (const d of data) {
|
|
4903
|
+
if (d.module.testType) counts[d.module.testType] = (counts[d.module.testType] || 0) + 1;
|
|
4904
|
+
for (const p of d.problems) problemCounts[p]++;
|
|
4905
|
+
}
|
|
4906
|
+
const totalProblems = data.filter(d => d.problems.length > 0).length;
|
|
4907
|
+
|
|
4908
|
+
const typeFilters = [
|
|
4909
|
+
{ key: 'all', label: 'Все', count: counts.all, color: '#7d8590' },
|
|
4910
|
+
{ key: 'unit', label: 'Unit', count: counts.unit, color: '#e3b341' },
|
|
4911
|
+
{ key: 'integration', label: 'Integration', count: counts.integration, color: '#58a6ff' },
|
|
4912
|
+
{ key: 'e2e', label: 'E2E', count: counts.e2e, color: '#d2a8ff' },
|
|
4913
|
+
];
|
|
4914
|
+
|
|
4915
|
+
const problemFilters = [
|
|
4916
|
+
{ key: null, label: 'Все тесты', count: counts.all },
|
|
4917
|
+
{ key: 'no-feature', label: 'Без фичи', count: problemCounts['no-feature'], cls: 'red' },
|
|
4918
|
+
{ key: 'no-source', label: 'Без исходника', count: problemCounts['no-source'], cls: 'red' },
|
|
4919
|
+
{ key: 'duplicate', label: 'Дубли', count: problemCounts.duplicate, cls: 'yellow' },
|
|
4920
|
+
{ key: 'stale', label: 'Устаревшие', count: problemCounts.stale, cls: 'yellow' },
|
|
4921
|
+
];
|
|
4922
|
+
|
|
4923
|
+
const featureSet = new Map();
|
|
4924
|
+
for (const d of data) {
|
|
4925
|
+
for (const fk of d.features) {
|
|
4926
|
+
if (!featureSet.has(fk)) featureSet.set(fk, 0);
|
|
4927
|
+
featureSet.set(fk, featureSet.get(fk) + 1);
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
const noFeatureCount = problemCounts['no-feature'];
|
|
4931
|
+
|
|
4932
|
+
extra.innerHTML = `
|
|
4933
|
+
<div class="tn-sidebar-section">
|
|
4934
|
+
<div class="tn-sidebar-label">Тип</div>
|
|
4935
|
+
${typeFilters.map(f => `
|
|
4936
|
+
<div class="tn-filter ${testNavType === f.key ? 'active' : ''}" data-tn-type="${f.key}">
|
|
4937
|
+
<span class="type-dot" style="background:${f.color}"></span>
|
|
4938
|
+
${f.label}
|
|
4939
|
+
<span class="tn-filter-count">${f.count}</span>
|
|
4940
|
+
</div>
|
|
4941
|
+
`).join('')}
|
|
4942
|
+
</div>
|
|
4943
|
+
<div class="tn-sidebar-section">
|
|
4944
|
+
<div class="tn-sidebar-label">Проблемы</div>
|
|
4945
|
+
${problemFilters.map(f => `
|
|
4946
|
+
<div class="tn-filter ${testNavProblem === f.key ? 'active' : ''}" data-tn-problem="${f.key || ''}">
|
|
4947
|
+
${f.label}
|
|
4948
|
+
<span class="tn-filter-count ${f.cls || ''}">${f.count}</span>
|
|
4949
|
+
</div>
|
|
4950
|
+
`).join('')}
|
|
4951
|
+
</div>
|
|
4952
|
+
<div class="tn-sidebar-section">
|
|
4953
|
+
<div class="tn-sidebar-label">Фича</div>
|
|
4954
|
+
<div class="tn-filter ${testNavFeature === null ? 'active' : ''}" data-tn-feature="">
|
|
4955
|
+
Все фичи
|
|
4956
|
+
<span class="tn-filter-count">${counts.all}</span>
|
|
4957
|
+
</div>
|
|
4958
|
+
${Array.from(featureSet.entries()).map(([fk, cnt]) => {
|
|
4959
|
+
const feat = D.features.find(f => f.key === fk);
|
|
4960
|
+
const color = feat ? feat.color : '#484f58';
|
|
4961
|
+
const label = feat ? feat.label : fk;
|
|
4962
|
+
return `<div class="tn-filter ${testNavFeature === fk ? 'active' : ''}" data-tn-feature="${fk}">
|
|
4963
|
+
<span class="type-dot" style="background:${color}"></span>
|
|
4964
|
+
${label}
|
|
4965
|
+
<span class="tn-filter-count">${cnt}</span>
|
|
4966
|
+
</div>`;
|
|
4967
|
+
}).join('')}
|
|
4968
|
+
${noFeatureCount > 0 ? `
|
|
4969
|
+
<div class="tn-filter ${testNavFeature === '__none__' ? 'active' : ''}" data-tn-feature="__none__">
|
|
4970
|
+
<span class="type-dot" style="background:#484f58"></span>
|
|
4971
|
+
(Без фичи)
|
|
4972
|
+
<span class="tn-filter-count red">${noFeatureCount}</span>
|
|
4973
|
+
</div>` : ''}
|
|
4974
|
+
</div>
|
|
4975
|
+
`;
|
|
4976
|
+
|
|
4977
|
+
extra.querySelectorAll('[data-tn-type]').forEach(el => {
|
|
4978
|
+
el.onclick = () => { testNavType = el.dataset.tnType; renderSidebar(); renderContent(); };
|
|
4979
|
+
});
|
|
4980
|
+
extra.querySelectorAll('[data-tn-problem]').forEach(el => {
|
|
4981
|
+
el.onclick = () => { testNavProblem = el.dataset.tnProblem || null; renderSidebar(); renderContent(); };
|
|
4982
|
+
});
|
|
4983
|
+
extra.querySelectorAll('[data-tn-feature]').forEach(el => {
|
|
4984
|
+
el.onclick = () => {
|
|
4985
|
+
const v = el.dataset.tnFeature;
|
|
4986
|
+
testNavFeature = v === '' ? null : v;
|
|
4987
|
+
renderSidebar(); renderContent();
|
|
4988
|
+
};
|
|
4989
|
+
});
|
|
4990
|
+
}
|
|
4991
|
+
|
|
4992
|
+
function renderTestNavigator(c) {
|
|
4993
|
+
const data = buildTestNavData();
|
|
4994
|
+
// apply filters
|
|
4995
|
+
let filtered = data;
|
|
4996
|
+
if (testNavType !== 'all') filtered = filtered.filter(d => d.module.testType === testNavType);
|
|
4997
|
+
if (testNavProblem) filtered = filtered.filter(d => d.problems.includes(testNavProblem));
|
|
4998
|
+
if (testNavFeature === '__none__') filtered = filtered.filter(d => d.features.length === 0);
|
|
4999
|
+
else if (testNavFeature) filtered = filtered.filter(d => d.features.includes(testNavFeature));
|
|
5000
|
+
|
|
5001
|
+
const q = searchQuery.toLowerCase();
|
|
5002
|
+
if (q) filtered = filtered.filter(d =>
|
|
5003
|
+
d.module.name.toLowerCase().includes(q) || d.module.relativePath.toLowerCase().includes(q)
|
|
5004
|
+
);
|
|
5005
|
+
|
|
5006
|
+
// stats
|
|
5007
|
+
const totalProblems = filtered.filter(d => d.problems.length > 0).length;
|
|
5008
|
+
const staleCount = filtered.filter(d => d.problems.includes('stale')).length;
|
|
5009
|
+
const typeCounts = { unit: 0, integration: 0, e2e: 0 };
|
|
5010
|
+
for (const d of filtered) { if (d.module.testType) typeCounts[d.module.testType]++; }
|
|
5011
|
+
|
|
5012
|
+
const rowsRenderKey = `testnav:${testNavType}:${testNavProblem}:${testNavFeature}:${q}`;
|
|
5013
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
5014
|
+
|
|
5015
|
+
c.innerHTML = `
|
|
5016
|
+
<div class="tn-summary">
|
|
5017
|
+
<div class="tn-stat-card">
|
|
5018
|
+
<div class="tn-stat-val">${filtered.length}</div>
|
|
5019
|
+
<div class="tn-stat-label">Тестов</div>
|
|
5020
|
+
</div>
|
|
5021
|
+
<div class="tn-stat-card">
|
|
5022
|
+
<div class="tn-stat-val" style="color:${totalProblems > 0 ? 'var(--red)' : 'var(--text)'}">${totalProblems}</div>
|
|
5023
|
+
<div class="tn-stat-label">С проблемами</div>
|
|
5024
|
+
</div>
|
|
5025
|
+
<div class="tn-stat-card">
|
|
5026
|
+
<div class="tn-stat-val" style="color:${staleCount > 0 ? 'var(--yellow)' : 'var(--text)'}">${staleCount}</div>
|
|
5027
|
+
<div class="tn-stat-label">Устаревших</div>
|
|
5028
|
+
</div>
|
|
5029
|
+
<div class="tn-stat-card">
|
|
5030
|
+
<div class="tn-stat-val" style="font-size:14px;line-height:1.8">
|
|
5031
|
+
<span style="color:#e3b341">${typeCounts.unit}</span> unit
|
|
5032
|
+
<span style="color:#58a6ff">${typeCounts.integration}</span> integ
|
|
5033
|
+
<span style="color:#d2a8ff">${typeCounts.e2e}</span> e2e
|
|
5034
|
+
</div>
|
|
5035
|
+
<div class="tn-stat-label">По типам</div>
|
|
5036
|
+
</div>
|
|
5037
|
+
</div>
|
|
5038
|
+
${!filtered.length ? '<div class="empty">Ничего не найдено</div>' : ''}
|
|
5039
|
+
<div class="tn-rows" id="tnRows"></div>
|
|
5040
|
+
${hasMoreRows ? `
|
|
5041
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
5042
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} тестов</span>
|
|
5043
|
+
<button class="bulk-actions-btn" id="tnLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
5044
|
+
</div>` : ''}
|
|
5045
|
+
`;
|
|
5046
|
+
|
|
5047
|
+
const rowsContainer = document.getElementById('tnRows');
|
|
5048
|
+
for (const d of visibleRows) {
|
|
5049
|
+
const row = document.createElement('div');
|
|
5050
|
+
const isActive = activePanelKey === d.module.id;
|
|
5051
|
+
const hasProblem = d.problems.length > 0;
|
|
5052
|
+
row.className = 'tn-row' + (isActive ? ' active' : '') + (hasProblem ? ' has-problem' : '');
|
|
5053
|
+
|
|
5054
|
+
const ttMeta = TEST_TYPE_META[d.module.testType];
|
|
5055
|
+
const typeIcon = ttMeta ? `<span class="type-dot" style="background:${ttMeta.color}"></span>` : '';
|
|
5056
|
+
const testCountBadge = d.module.testCount ? `<span class="tn-test-count">${d.module.testCount} case${d.module.testCount > 1 ? 's' : ''}</span>` : '';
|
|
5057
|
+
|
|
5058
|
+
const featureBadges = d.features.map(fk => {
|
|
5059
|
+
const feat = D.features.find(f => f.key === fk);
|
|
5060
|
+
const color = feat ? feat.color : '#484f58';
|
|
5061
|
+
const label = feat ? feat.label : fk;
|
|
5062
|
+
return `<span class="tn-feature-badge" data-feature-key="${fk}" title="${label}"><span class="type-dot" style="background:${color}"></span>${label}</span>`;
|
|
5063
|
+
}).join('');
|
|
5064
|
+
|
|
5065
|
+
let sourceHtml = '';
|
|
5066
|
+
if (d.linkedSources.length > 0) {
|
|
5067
|
+
const src = d.linkedSources[0];
|
|
5068
|
+
sourceHtml = `<span class="tn-source-link" data-source-id="${src.id}" title="${src.relativePath}">${src.name}</span>`;
|
|
5069
|
+
if (d.linkedSources.length > 1) sourceHtml += `<span class="tn-source-link"> +${d.linkedSources.length - 1}</span>`;
|
|
5070
|
+
} else {
|
|
5071
|
+
sourceHtml = '<span class="tn-no-source">нет исходника</span>';
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
const problemBadges = d.problems.map(p => {
|
|
5075
|
+
const labels = { 'no-feature': 'без фичи', 'no-source': 'без исходника', duplicate: 'дубль', stale: 'устарел' };
|
|
5076
|
+
const cls = p === 'stale' ? 'stale' : p === 'duplicate' ? 'duplicate' : 'orphan';
|
|
5077
|
+
return `<span class="tn-problem-badge ${cls}">${labels[p]}</span>`;
|
|
5078
|
+
}).join('');
|
|
5079
|
+
|
|
5080
|
+
const dir = d.module.relativePath.replace(/[^/\\]+$/, '').replace(/[/\\]$/, '');
|
|
5081
|
+
row.innerHTML = `
|
|
5082
|
+
${typeIcon}
|
|
5083
|
+
<span class="tn-row-name">${d.module.name}</span>
|
|
5084
|
+
${testCountBadge}
|
|
5085
|
+
<div class="tn-badges">${featureBadges}${problemBadges}</div>
|
|
5086
|
+
${sourceHtml}
|
|
5087
|
+
<span class="tn-row-dir">${dir}</span>
|
|
5088
|
+
`;
|
|
5089
|
+
|
|
5090
|
+
row.onclick = (e) => {
|
|
5091
|
+
if (e.target.closest('[data-feature-key]')) {
|
|
5092
|
+
const fk = e.target.closest('[data-feature-key]').dataset.featureKey;
|
|
5093
|
+
view = 'features';
|
|
5094
|
+
drillFeatureKey = fk;
|
|
5095
|
+
drillTestType = null;
|
|
5096
|
+
window.location.hash = 'feature=' + fk;
|
|
5097
|
+
renderSidebar();
|
|
5098
|
+
renderContent();
|
|
5099
|
+
return;
|
|
5100
|
+
}
|
|
5101
|
+
if (e.target.closest('[data-source-id]')) {
|
|
5102
|
+
const sid = e.target.closest('[data-source-id]').dataset.sourceId;
|
|
5103
|
+
const srcMod = D.modules.find(m => m.id === sid);
|
|
5104
|
+
if (srcMod) openModulePanel(srcMod);
|
|
5105
|
+
return;
|
|
5106
|
+
}
|
|
5107
|
+
openModulePanel(d.module);
|
|
5108
|
+
};
|
|
5109
|
+
rowsContainer.appendChild(row);
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
const loadMoreBtn = document.getElementById('tnLoadMoreBtn');
|
|
5113
|
+
if (loadMoreBtn) {
|
|
5114
|
+
loadMoreBtn.onclick = () => { increaseFileRowsLimit(); renderContent(); };
|
|
5115
|
+
}
|
|
5116
|
+
}
|
|
5117
|
+
|
|
4784
5118
|
function renderModuleGrid(c) {
|
|
4785
5119
|
const q = searchQuery.toLowerCase();
|
|
4786
5120
|
const list = D.modules.filter(m => {
|
|
@@ -5074,6 +5408,9 @@ document.querySelectorAll('.view-tab').forEach(tab => {
|
|
|
5074
5408
|
activePanelKey = null;
|
|
5075
5409
|
searchQuery = '';
|
|
5076
5410
|
activeTypes.clear();
|
|
5411
|
+
testNavType = 'all';
|
|
5412
|
+
testNavFeature = null;
|
|
5413
|
+
testNavProblem = null;
|
|
5077
5414
|
document.getElementById('searchInput').value = '';
|
|
5078
5415
|
document.getElementById('panel').classList.remove('open');
|
|
5079
5416
|
if (view !== 'features') clearFeatureHash();
|