fix(dashboard): prevent service sections from falling back to proxy outbound view and showing latency test
This commit is contained in:
@@ -1473,6 +1473,76 @@ export async function getDashboardSections(
|
||||
value,
|
||||
}),
|
||||
);
|
||||
const serviceStatusCache = new Map<
|
||||
string,
|
||||
Promise<Tachyon.ServiceStatus | undefined>
|
||||
>();
|
||||
|
||||
const getServiceStatus = (serviceType: 'zapret' | 'zapret2' | 'byedpi') => {
|
||||
if (!serviceStatusCache.has(serviceType)) {
|
||||
serviceStatusCache.set(
|
||||
serviceType,
|
||||
(async () => {
|
||||
try {
|
||||
if (serviceType === 'zapret') {
|
||||
const result = await TachyonShellMethods.getZapretStatus();
|
||||
if (result.success && result.data) {
|
||||
const s = result.data;
|
||||
return {
|
||||
serviceType: 'zapret',
|
||||
configured: Boolean(s.configured),
|
||||
ready: Boolean(s.ready),
|
||||
conflict: Boolean(s.conflict),
|
||||
runningProcesses: s.running_process_count,
|
||||
expectedProcesses: s.expected_process_count,
|
||||
restartCount: s.restart_count,
|
||||
unstable: Boolean(s.runtime_unstable),
|
||||
statusMessage: s.status_message,
|
||||
};
|
||||
}
|
||||
} else if (serviceType === 'zapret2') {
|
||||
const result = await TachyonShellMethods.getZapret2Status();
|
||||
if (result.success && result.data) {
|
||||
const s = result.data;
|
||||
return {
|
||||
serviceType: 'zapret2',
|
||||
configured: Boolean(s.configured),
|
||||
ready: Boolean(s.ready),
|
||||
conflict: Boolean(s.conflict),
|
||||
runningProcesses: s.running_process_count,
|
||||
expectedProcesses: s.expected_process_count,
|
||||
restartCount: 0,
|
||||
unstable: false,
|
||||
statusMessage: s.status_message,
|
||||
};
|
||||
}
|
||||
} else if (serviceType === 'byedpi') {
|
||||
const result = await TachyonShellMethods.getByedpiStatus();
|
||||
if (result.success && result.data) {
|
||||
const s = result.data;
|
||||
return {
|
||||
serviceType: 'byedpi',
|
||||
configured: Boolean(s.configured),
|
||||
ready: Boolean(s.ready),
|
||||
conflict: Boolean(s.conflict),
|
||||
runningProcesses: s.running_process_count,
|
||||
expectedProcesses: s.expected_process_count,
|
||||
restartCount: s.restart_count,
|
||||
unstable: Boolean(s.runtime_unstable),
|
||||
statusMessage: s.status_message,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// Status fetch failed — show section without status
|
||||
}
|
||||
return undefined;
|
||||
})(),
|
||||
);
|
||||
}
|
||||
return serviceStatusCache.get(serviceType)!;
|
||||
};
|
||||
|
||||
const data = await Promise.all(
|
||||
configSections
|
||||
.filter(
|
||||
@@ -1599,62 +1669,7 @@ export async function getDashboardSections(
|
||||
|
||||
if (isServiceAction(sectionAction)) {
|
||||
const serviceType = sectionAction as 'zapret' | 'zapret2' | 'byedpi';
|
||||
|
||||
let serviceStatus: Tachyon.ServiceStatus | undefined;
|
||||
|
||||
try {
|
||||
if (serviceType === 'zapret') {
|
||||
const result = await TachyonShellMethods.getZapretStatus();
|
||||
if (result.success) {
|
||||
const s = result.data;
|
||||
serviceStatus = {
|
||||
serviceType: 'zapret',
|
||||
configured: Boolean(s.configured),
|
||||
ready: Boolean(s.ready),
|
||||
conflict: Boolean(s.conflict),
|
||||
runningProcesses: s.running_process_count,
|
||||
expectedProcesses: s.expected_process_count,
|
||||
restartCount: s.restart_count,
|
||||
unstable: Boolean(s.runtime_unstable),
|
||||
statusMessage: s.status_message,
|
||||
};
|
||||
}
|
||||
} else if (serviceType === 'zapret2') {
|
||||
const result = await TachyonShellMethods.getZapret2Status();
|
||||
if (result.success) {
|
||||
const s = result.data;
|
||||
serviceStatus = {
|
||||
serviceType: 'zapret2',
|
||||
configured: Boolean(s.configured),
|
||||
ready: Boolean(s.ready),
|
||||
conflict: Boolean(s.conflict),
|
||||
runningProcesses: s.running_process_count,
|
||||
expectedProcesses: s.expected_process_count,
|
||||
restartCount: 0,
|
||||
unstable: false,
|
||||
statusMessage: s.status_message,
|
||||
};
|
||||
}
|
||||
} else if (serviceType === 'byedpi') {
|
||||
const result = await TachyonShellMethods.getByedpiStatus();
|
||||
if (result.success) {
|
||||
const s = result.data;
|
||||
serviceStatus = {
|
||||
serviceType: 'byedpi',
|
||||
configured: Boolean(s.configured),
|
||||
ready: Boolean(s.ready),
|
||||
conflict: Boolean(s.conflict),
|
||||
runningProcesses: s.running_process_count,
|
||||
expectedProcesses: s.expected_process_count,
|
||||
restartCount: s.restart_count,
|
||||
unstable: Boolean(s.runtime_unstable),
|
||||
statusMessage: s.status_message,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
// Status fetch failed — show section without status
|
||||
}
|
||||
const serviceStatus = await getServiceStatus(serviceType);
|
||||
|
||||
const statusLabel = serviceStatus
|
||||
? serviceStatus.ready
|
||||
|
||||
@@ -277,9 +277,142 @@ function renderDefaultState({
|
||||
const isConnectionNode = ['vpn', 'awg', 'warp'].includes(
|
||||
section.action || '',
|
||||
);
|
||||
const isServiceNode = ['zapret', 'zapret2', 'byedpi'].includes(
|
||||
section.action || '',
|
||||
);
|
||||
const isServiceNode =
|
||||
['zapret', 'zapret2', 'byedpi'].includes(section.action || '') ||
|
||||
Boolean(section.serviceStatus);
|
||||
|
||||
if (isServiceNode) {
|
||||
const ss = section.serviceStatus;
|
||||
const serviceType =
|
||||
ss?.serviceType ||
|
||||
(['zapret', 'zapret2', 'byedpi'].includes(section.action || '')
|
||||
? (section.action as 'zapret' | 'zapret2' | 'byedpi')
|
||||
: 'zapret');
|
||||
const typeLabel =
|
||||
serviceType === 'zapret'
|
||||
? 'Zapret'
|
||||
: serviceType === 'zapret2'
|
||||
? 'Zapret2'
|
||||
: 'ByeDPI';
|
||||
const statusColor = ss
|
||||
? ss.ready
|
||||
? 'var(--success-color-medium, green)'
|
||||
: ss.conflict
|
||||
? 'var(--error-color-medium, red)'
|
||||
: ss.configured
|
||||
? 'var(--warn-color-medium, orange)'
|
||||
: 'var(--primary-color-low, lightgray)'
|
||||
: 'var(--primary-color-low, lightgray)';
|
||||
const statusText = ss
|
||||
? ss.ready
|
||||
? _('Running')
|
||||
: ss.conflict
|
||||
? _('Conflict')
|
||||
: ss.configured
|
||||
? _('Stopped')
|
||||
: _('Not configured')
|
||||
: _('Unknown');
|
||||
|
||||
return E('div', { class: 'tachyon_dashboard-page__outbound-section' }, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
class: 'tachyon_dashboard-page__outbound-section__title-section',
|
||||
style: 'cursor: default;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
class:
|
||||
'tachyon_dashboard-page__outbound-section__title-section__title',
|
||||
style: 'display: flex; align-items: center; gap: 8px;',
|
||||
},
|
||||
[
|
||||
E('span', {}, section.displayName),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
style: 'font-size: 12px; opacity: 0.6; font-weight: normal;',
|
||||
},
|
||||
typeLabel,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 16px 12px;',
|
||||
},
|
||||
[
|
||||
E('div', { style: 'display: flex; align-items: center; gap: 6px;' }, [
|
||||
E(
|
||||
'span',
|
||||
{ style: 'opacity: 0.7; font-size: 13px;' },
|
||||
_('Status') + ':',
|
||||
),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
style: `font-size: 13px; font-weight: 500; color: ${statusColor};`,
|
||||
},
|
||||
statusText,
|
||||
),
|
||||
]),
|
||||
ss && ss.restartCount > 0
|
||||
? E(
|
||||
'div',
|
||||
{ style: 'display: flex; align-items: center; gap: 6px;' },
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{ style: 'opacity: 0.7; font-size: 13px;' },
|
||||
_('Restarts') + ':',
|
||||
),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
style:
|
||||
'font-size: 13px; font-weight: 500; color: var(--warn-color-medium, orange);',
|
||||
},
|
||||
`${ss.restartCount}`,
|
||||
),
|
||||
],
|
||||
)
|
||||
: '',
|
||||
ss && ss.unstable
|
||||
? E(
|
||||
'div',
|
||||
{ style: 'display: flex; align-items: center; gap: 6px;' },
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
style:
|
||||
'font-size: 13px; font-weight: 500; color: var(--error-color-medium, red);',
|
||||
},
|
||||
_('Unstable'),
|
||||
),
|
||||
],
|
||||
)
|
||||
: '',
|
||||
],
|
||||
),
|
||||
ss?.statusMessage
|
||||
? E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;',
|
||||
},
|
||||
formatServiceStatusMessage(ss.statusMessage),
|
||||
)
|
||||
: '',
|
||||
]);
|
||||
}
|
||||
|
||||
function testLatency() {
|
||||
if (section.withTagSelect) {
|
||||
@@ -606,130 +739,6 @@ function renderDefaultState({
|
||||
);
|
||||
}
|
||||
|
||||
if (isServiceNode && section.serviceStatus) {
|
||||
const ss = section.serviceStatus;
|
||||
const statusColor = ss.ready
|
||||
? 'var(--success-color-medium, green)'
|
||||
: ss.conflict
|
||||
? 'var(--error-color-medium, red)'
|
||||
: ss.configured
|
||||
? 'var(--warn-color-medium, orange)'
|
||||
: 'var(--primary-color-low, lightgray)';
|
||||
const statusText = ss.ready
|
||||
? _('Running')
|
||||
: ss.conflict
|
||||
? _('Conflict')
|
||||
: ss.configured
|
||||
? _('Stopped')
|
||||
: _('Not configured');
|
||||
const typeLabel =
|
||||
ss.serviceType === 'zapret'
|
||||
? 'Zapret'
|
||||
: ss.serviceType === 'zapret2'
|
||||
? 'Zapret2'
|
||||
: 'ByeDPI';
|
||||
|
||||
return E('div', { class: 'tachyon_dashboard-page__outbound-section' }, [
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
class: 'tachyon_dashboard-page__outbound-section__title-section',
|
||||
style: 'cursor: default;',
|
||||
},
|
||||
[
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
class:
|
||||
'tachyon_dashboard-page__outbound-section__title-section__title',
|
||||
style: 'display: flex; align-items: center; gap: 8px;',
|
||||
},
|
||||
[
|
||||
E('span', {}, section.displayName),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
style: 'font-size: 12px; opacity: 0.6; font-weight: normal;',
|
||||
},
|
||||
typeLabel,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 16px 12px;',
|
||||
},
|
||||
[
|
||||
E('div', { style: 'display: flex; align-items: center; gap: 6px;' }, [
|
||||
E(
|
||||
'span',
|
||||
{ style: 'opacity: 0.7; font-size: 13px;' },
|
||||
_('Status') + ':',
|
||||
),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
style: `font-size: 13px; font-weight: 500; color: ${statusColor};`,
|
||||
},
|
||||
statusText,
|
||||
),
|
||||
]),
|
||||
ss.restartCount > 0
|
||||
? E(
|
||||
'div',
|
||||
{ style: 'display: flex; align-items: center; gap: 6px;' },
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{ style: 'opacity: 0.7; font-size: 13px;' },
|
||||
_('Restarts') + ':',
|
||||
),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
style:
|
||||
'font-size: 13px; font-weight: 500; color: var(--warn-color-medium, orange);',
|
||||
},
|
||||
`${ss.restartCount}`,
|
||||
),
|
||||
],
|
||||
)
|
||||
: '',
|
||||
ss.unstable
|
||||
? E(
|
||||
'div',
|
||||
{ style: 'display: flex; align-items: center; gap: 6px;' },
|
||||
[
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
style:
|
||||
'font-size: 13px; font-weight: 500; color: var(--error-color-medium, red);',
|
||||
},
|
||||
_('Unstable'),
|
||||
),
|
||||
],
|
||||
)
|
||||
: '',
|
||||
],
|
||||
),
|
||||
ss.statusMessage
|
||||
? E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;',
|
||||
},
|
||||
formatServiceStatusMessage(ss.statusMessage),
|
||||
)
|
||||
: '',
|
||||
]);
|
||||
}
|
||||
|
||||
const metadataNodes = (section.subscriptionMetadata || [])
|
||||
.map((metadata) => renderSubscriptionMetadata(metadata))
|
||||
.filter(Boolean) as HTMLElement[];
|
||||
@@ -739,6 +748,11 @@ function renderDefaultState({
|
||||
onUpdateSubscription,
|
||||
);
|
||||
|
||||
const canTestLatency =
|
||||
!isServiceNode &&
|
||||
(section.withTagSelect ||
|
||||
(section.outbounds && section.outbounds.length > 0));
|
||||
|
||||
return E('div', { class: 'tachyon_dashboard-page__outbound-section' }, [
|
||||
// Title with test latency
|
||||
E(
|
||||
@@ -867,47 +881,52 @@ function renderDefaultState({
|
||||
},
|
||||
[
|
||||
...(subscriptionUpdateAction ? [subscriptionUpdateAction] : []),
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
class: 'btn dashboard-sections-grid-item-test-latency',
|
||||
'data-latency-section': section.sectionName,
|
||||
disabled: latencyFetching ? true : undefined,
|
||||
click: (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (latencyFetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
testLatency();
|
||||
},
|
||||
},
|
||||
latencyFetching
|
||||
? [
|
||||
renderLoaderCircleIcon24(),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
class:
|
||||
'dashboard-sections-grid-item-test-latency__label',
|
||||
},
|
||||
isConnectionNode
|
||||
? _('Checking...')
|
||||
: getLatencyTestLabel(latencyProgress),
|
||||
),
|
||||
]
|
||||
: E(
|
||||
'span',
|
||||
...(canTestLatency
|
||||
? [
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
class: 'dashboard-sections-grid-item-test-latency__label',
|
||||
type: 'button',
|
||||
class: 'btn dashboard-sections-grid-item-test-latency',
|
||||
'data-latency-section': section.sectionName,
|
||||
disabled: latencyFetching ? true : undefined,
|
||||
click: (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (latencyFetching) {
|
||||
return;
|
||||
}
|
||||
|
||||
testLatency();
|
||||
},
|
||||
},
|
||||
isConnectionNode
|
||||
? _('Check Connection')
|
||||
: _('Test latency'),
|
||||
latencyFetching
|
||||
? [
|
||||
renderLoaderCircleIcon24(),
|
||||
E(
|
||||
'span',
|
||||
{
|
||||
class:
|
||||
'dashboard-sections-grid-item-test-latency__label',
|
||||
},
|
||||
isConnectionNode
|
||||
? _('Checking...')
|
||||
: getLatencyTestLabel(latencyProgress),
|
||||
),
|
||||
]
|
||||
: E(
|
||||
'span',
|
||||
{
|
||||
class:
|
||||
'dashboard-sections-grid-item-test-latency__label',
|
||||
},
|
||||
isConnectionNode
|
||||
? _('Check Connection')
|
||||
: _('Test latency'),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
|
||||
|
||||
class MockMutationObserver {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
takeRecords() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
globalThis.MutationObserver = MockMutationObserver as any;
|
||||
|
||||
function createDummyElement(tag: string) {
|
||||
const children: any[] = [];
|
||||
return {
|
||||
tagName: tag.toUpperCase(),
|
||||
children,
|
||||
childNodes: children,
|
||||
style: {} as Record<string, string>,
|
||||
className: '',
|
||||
textContent: '',
|
||||
setAttribute(name: string, val: string) {
|
||||
(this as any)[name] = val;
|
||||
},
|
||||
getAttribute(name: string) {
|
||||
return (this as any)[name] ?? null;
|
||||
},
|
||||
querySelectorAll(selector: string): any[] {
|
||||
const results: any[] = [];
|
||||
function traverse(node: any) {
|
||||
if (!node || typeof node !== 'object') return;
|
||||
if (selector.startsWith('.')) {
|
||||
const targetClass = selector.slice(1);
|
||||
const classes = (node.class || node.className || '').split(/\s+/);
|
||||
if (classes.includes(targetClass)) {
|
||||
results.push(node);
|
||||
}
|
||||
}
|
||||
if (selector === 'button' && node.tagName === 'BUTTON') {
|
||||
results.push(node);
|
||||
}
|
||||
if (selector === 'svg' && node.tagName === 'SVG') {
|
||||
results.push(node);
|
||||
}
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) {
|
||||
traverse(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(this);
|
||||
return results;
|
||||
},
|
||||
querySelector(selector: string): any {
|
||||
return this.querySelectorAll(selector)[0] || null;
|
||||
},
|
||||
appendChild(node: any) {
|
||||
children.push(node);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
globalThis.document = {
|
||||
body: createDummyElement('body') as any,
|
||||
createElement: (tag: string) => createDummyElement(tag) as any,
|
||||
createElementNS: (_ns: string, tag: string) => createDummyElement(tag) as any,
|
||||
querySelectorAll: () => [],
|
||||
querySelector: () => null,
|
||||
getElementById: () => null,
|
||||
} as any;
|
||||
|
||||
(globalThis as any).rpc = { declare: vi.fn() };
|
||||
(globalThis as any).uci = { sections: vi.fn().mockResolvedValue([]) };
|
||||
(globalThis as any).localStorage = { getItem: vi.fn(), setItem: vi.fn() };
|
||||
|
||||
(globalThis as any).E = (tag: string, attrs?: any, children?: any) => {
|
||||
const el = (globalThis as any).document.createElement(tag);
|
||||
if (attrs) {
|
||||
Object.assign(el, attrs);
|
||||
}
|
||||
if (children) {
|
||||
const list = Array.isArray(children) ? children : [children];
|
||||
for (const child of list) {
|
||||
if (child !== null && child !== undefined && child !== '') {
|
||||
el.appendChild(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
let renderSections: any;
|
||||
|
||||
describe('renderSections', () => {
|
||||
beforeAll(async () => {
|
||||
const mod = await import('../renderSections');
|
||||
renderSections = mod.renderSections;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders service section without serviceStatus as a clean service card without latency button', () => {
|
||||
const section: any = {
|
||||
code: 'discord',
|
||||
sectionName: 'discord',
|
||||
displayName: 'Discord',
|
||||
action: 'zapret2',
|
||||
withTagSelect: false,
|
||||
serviceStatus: undefined,
|
||||
outbounds: [
|
||||
{
|
||||
code: 'discord',
|
||||
displayName: 'Unknown',
|
||||
latency: 0,
|
||||
type: 'ZAPRET2',
|
||||
selected: true,
|
||||
canCopyLink: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const el = renderSections({
|
||||
loading: false,
|
||||
failed: false,
|
||||
section,
|
||||
onTestLatency: vi.fn(),
|
||||
onChooseOutbound: vi.fn(),
|
||||
onCopyOutbound: vi.fn(),
|
||||
onShowUrlTestInfo: vi.fn(),
|
||||
onShowPriorityInfo: vi.fn(),
|
||||
onUpdateSubscription: vi.fn(),
|
||||
latencyFetching: false,
|
||||
subscriptionUpdating: false,
|
||||
});
|
||||
|
||||
const latencyButtons = el.querySelectorAll(
|
||||
'.dashboard-sections-grid-item-test-latency',
|
||||
);
|
||||
expect(latencyButtons).toHaveLength(0);
|
||||
|
||||
const buttons = el.querySelectorAll('button');
|
||||
expect(buttons).toHaveLength(0);
|
||||
|
||||
// Should not render chevron svg
|
||||
const svgs = el.querySelectorAll('svg');
|
||||
expect(svgs).toHaveLength(0);
|
||||
|
||||
// Should not render outbound grid
|
||||
const outboundGrid = el.querySelectorAll(
|
||||
'.tachyon_dashboard-page__outbound-grid',
|
||||
);
|
||||
expect(outboundGrid).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders service section with serviceStatus as a service card without latency button', () => {
|
||||
const section: any = {
|
||||
code: 'youtube',
|
||||
sectionName: 'youtube',
|
||||
displayName: 'Youtube',
|
||||
action: 'zapret2',
|
||||
withTagSelect: false,
|
||||
serviceStatus: {
|
||||
serviceType: 'zapret2',
|
||||
configured: true,
|
||||
ready: true,
|
||||
conflict: false,
|
||||
runningProcesses: 1,
|
||||
expectedProcesses: 1,
|
||||
restartCount: 0,
|
||||
unstable: false,
|
||||
statusMessage: 'zapret2 provider status is normal',
|
||||
},
|
||||
outbounds: [],
|
||||
};
|
||||
|
||||
const el = renderSections({
|
||||
loading: false,
|
||||
failed: false,
|
||||
section,
|
||||
onTestLatency: vi.fn(),
|
||||
onChooseOutbound: vi.fn(),
|
||||
onCopyOutbound: vi.fn(),
|
||||
onShowUrlTestInfo: vi.fn(),
|
||||
onShowPriorityInfo: vi.fn(),
|
||||
onUpdateSubscription: vi.fn(),
|
||||
latencyFetching: false,
|
||||
subscriptionUpdating: false,
|
||||
});
|
||||
|
||||
const latencyButtons = el.querySelectorAll(
|
||||
'.dashboard-sections-grid-item-test-latency',
|
||||
);
|
||||
expect(latencyButtons).toHaveLength(0);
|
||||
|
||||
const buttons = el.querySelectorAll('button');
|
||||
expect(buttons).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('renders zapret and byedpi service sections without latency button', () => {
|
||||
for (const action of ['zapret', 'byedpi']) {
|
||||
const section: any = {
|
||||
code: action,
|
||||
sectionName: action,
|
||||
displayName: action.toUpperCase(),
|
||||
action,
|
||||
withTagSelect: false,
|
||||
outbounds: [],
|
||||
};
|
||||
|
||||
const el = renderSections({
|
||||
loading: false,
|
||||
failed: false,
|
||||
section,
|
||||
onTestLatency: vi.fn(),
|
||||
onChooseOutbound: vi.fn(),
|
||||
onCopyOutbound: vi.fn(),
|
||||
onShowUrlTestInfo: vi.fn(),
|
||||
onShowPriorityInfo: vi.fn(),
|
||||
onUpdateSubscription: vi.fn(),
|
||||
latencyFetching: false,
|
||||
subscriptionUpdating: false,
|
||||
});
|
||||
|
||||
const latencyButtons = el.querySelectorAll(
|
||||
'.dashboard-sections-grid-item-test-latency',
|
||||
);
|
||||
expect(latencyButtons).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('renders latency test button for proxy outbound sections with outbounds', () => {
|
||||
const section: any = {
|
||||
code: 'proxy-group',
|
||||
sectionName: 'main',
|
||||
displayName: 'Main Proxy',
|
||||
action: 'connection',
|
||||
withTagSelect: true,
|
||||
outbounds: [
|
||||
{
|
||||
code: 'node-1',
|
||||
displayName: 'Node 1',
|
||||
latency: 120,
|
||||
type: 'VLESS',
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const el = renderSections({
|
||||
loading: false,
|
||||
failed: false,
|
||||
section,
|
||||
onTestLatency: vi.fn(),
|
||||
onChooseOutbound: vi.fn(),
|
||||
onCopyOutbound: vi.fn(),
|
||||
onShowUrlTestInfo: vi.fn(),
|
||||
onShowPriorityInfo: vi.fn(),
|
||||
onUpdateSubscription: vi.fn(),
|
||||
latencyFetching: false,
|
||||
subscriptionUpdating: false,
|
||||
});
|
||||
|
||||
const latencyButtons = el.querySelectorAll(
|
||||
'.dashboard-sections-grid-item-test-latency',
|
||||
);
|
||||
expect(latencyButtons).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not render latency test button for empty proxy section', () => {
|
||||
const section: any = {
|
||||
code: 'empty-group',
|
||||
sectionName: 'empty',
|
||||
displayName: 'Empty Group',
|
||||
action: 'connection',
|
||||
withTagSelect: false,
|
||||
outbounds: [],
|
||||
};
|
||||
|
||||
const el = renderSections({
|
||||
loading: false,
|
||||
failed: false,
|
||||
section,
|
||||
onTestLatency: vi.fn(),
|
||||
onChooseOutbound: vi.fn(),
|
||||
onCopyOutbound: vi.fn(),
|
||||
onShowUrlTestInfo: vi.fn(),
|
||||
onShowPriorityInfo: vi.fn(),
|
||||
onUpdateSubscription: vi.fn(),
|
||||
latencyFetching: false,
|
||||
subscriptionUpdating: false,
|
||||
});
|
||||
|
||||
const latencyButtons = el.querySelectorAll(
|
||||
'.dashboard-sections-grid-item-test-latency',
|
||||
);
|
||||
expect(latencyButtons).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -127,29 +127,39 @@ export async function runSectionsCheck() {
|
||||
|
||||
// Service sections (zapret/zapret2/byedpi) don't have Clash API proxies.
|
||||
// Use the serviceStatus that was already fetched in getDashboardSections.
|
||||
if (section.serviceStatus) {
|
||||
const s = section.serviceStatus;
|
||||
if (s.ready) {
|
||||
return {
|
||||
state: 'success',
|
||||
latency: _('Running'),
|
||||
};
|
||||
}
|
||||
if (s.conflict) {
|
||||
const isService =
|
||||
['zapret', 'zapret2', 'byedpi'].includes(section.action || '') ||
|
||||
Boolean(section.serviceStatus);
|
||||
|
||||
if (isService) {
|
||||
if (section.serviceStatus) {
|
||||
const s = section.serviceStatus;
|
||||
if (s.ready) {
|
||||
return {
|
||||
state: 'success',
|
||||
latency: _('Running'),
|
||||
};
|
||||
}
|
||||
if (s.conflict) {
|
||||
return {
|
||||
state: 'error',
|
||||
latency: _('Conflict'),
|
||||
};
|
||||
}
|
||||
if (s.configured) {
|
||||
return {
|
||||
state: 'warning',
|
||||
latency: _('Stopped'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: 'error',
|
||||
latency: _('Conflict'),
|
||||
};
|
||||
}
|
||||
if (s.configured) {
|
||||
return {
|
||||
state: 'warning',
|
||||
latency: _('Stopped'),
|
||||
latency: _('Not configured'),
|
||||
};
|
||||
}
|
||||
return {
|
||||
state: 'error',
|
||||
latency: _('Not configured'),
|
||||
state: 'warning',
|
||||
latency: _('Unknown'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user