Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Sysnode is designed to make that information easier to read, easier to verify, a
- Sentry Node trend data over selectable time ranges.
- Current governance proposals, budgets, support, vote counts, estimated payout-selection start, and next superblock.
- Country-level Sentry Node distribution.
- SYS market context including price, volume, market cap, supply, and exchange links.
- SYS market context including price, volume, market cap, and supply.
- Educational pages covering what Sentry Nodes are and how to approach setup.

## Tech Stack
Expand Down
2 changes: 2 additions & 0 deletions src/components/ExchangeGrid.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';

export default function ExchangeGrid(props) {
if (!Array.isArray(props.exchanges) || props.exchanges.length === 0) return null;

return (
<div className="market-exchanges market-exchanges--compact">
<div className="market-exchanges__header">
Expand Down
27 changes: 27 additions & 0 deletions src/components/ExchangeGrid.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import ExchangeGrid from './ExchangeGrid';
import SYS_EXCHANGES from '../data/exchanges';

test.each([[], undefined, null])('omits the exchange section and its market CTA without listings (%p)', (exchanges) => {
const { container } = render(<ExchangeGrid exchanges={exchanges} />);
expect(container).toBeEmptyDOMElement();
expect(screen.queryByText(/Get your Syscoin|View all markets/)).not.toBeInTheDocument();
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});

test('current exchange data contains no unverified or planned listings', () => {
expect(SYS_EXCHANGES).toEqual([]);
});

test('can still render an explicitly supplied listing', () => {
render(<ExchangeGrid exchanges={[{
name: 'Example exchange',
href: 'https://exchange.example/trade/SYS-USDT',
logo: 'https://exchange.example/logo.png',
}]} />);
const listing = screen.getByRole('link', { name: /Example exchange/ });
expect(listing).toHaveAttribute('href', 'https://exchange.example/trade/SYS-USDT');
expect(listing).toHaveAttribute('rel', 'noopener noreferrer');
expect(screen.getByAltText('Example exchange logo')).toBeInTheDocument();
});
35 changes: 3 additions & 32 deletions src/data/exchanges.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,5 @@
const SYS_EXCHANGES = [
{
name: 'Gate',
logo: 'https://coin-images.coingecko.com/markets/images/60/small/Frame_1.png?1747795534',
href: 'https://www.gate.com/trade/SYS_USDT',
},
{
name: 'HTX',
logo: 'https://coin-images.coingecko.com/markets/images/25/small/htx.png?1721712842',
href: 'https://www.htx.com/trade/sys_usdt',
},
{
name: 'MEXC',
logo: 'https://coin-images.coingecko.com/markets/images/409/small/164286be-32a5-4b58-978c-d072eea00eb9.jpeg?1775619316',
href: 'https://www.mexc.com/exchange/SYS_USDT',
},
{
name: 'Bitget',
logo: 'https://coin-images.coingecko.com/markets/images/540/small/2023-07-25_21.47.43.jpg?1706864507',
href: 'https://www.bitget.com/spot/SYSUSDT',
},
{
name: 'CoinEx',
logo: 'https://coin-images.coingecko.com/markets/images/135/small/coinex.jpg?1706864305',
href: 'https://www.coinex.com/en/exchange/sys-usdt',
},
{
name: 'KuCoin',
logo: 'https://coin-images.coingecko.com/markets/images/61/small/kucoin.png?1706864282',
href: 'https://www.kucoin.com/trade/SYS-USDT',
},
];
// No trading venue is currently verified for display. Keep this empty until
// a current listing is confirmed; planned venues must not appear as live.
const SYS_EXCHANGES = [];

export default SYS_EXCHANGES;
12 changes: 12 additions & 0 deletions src/pages/NetworkAvailability.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,15 @@ test('technical version and protocol identifiers keep their ungrouped representa
expect(screen.queryByText('320,000')).not.toBeInTheDocument();
expect(screen.queryByText('70,016')).not.toBeInTheDocument();
});

test('Home keeps market data and navigation without an empty exchange promotion', async () => {
fetchNetworkStats.mockResolvedValue(validNetworkStats());
render(<MemoryRouter><Home /></MemoryRouter>);
await screen.findByText('800,000,000 SYS');
expect(screen.getByText('Syscoin Market')).toBeInTheDocument();
expect(screen.getByText('SYS Price')).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'View Proposals' })).toHaveAttribute('href', '/governance');
expect(screen.queryByText(/Get your Syscoin|View all markets/)).not.toBeInTheDocument();
expect(document.querySelector('.market-exchanges')).toBeNull();
expect(document.querySelector('.panel--market')).toBeInTheDocument();
});
35 changes: 35 additions & 0 deletions tests/browser/network-availability.spec.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,38 @@ test('governance stats outage preserves proposals without false support at phone
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(321);
await page.screenshot({ path: testInfo.outputPath('governance-stats-unavailable.png'), fullPage: true });
});

for (const width of [320, 390]) {
test(`Home keeps market data without an empty exchange section at ${width}px`, async ({ page }, testInfo) => {
const now = Date.now();
await page.setViewportSize({ width, height: 640 });
await page.route('**/auth/me', request => request.fulfill({ status: 401, json: { error: 'unauthorized' } }));
await page.route('**/mnstats', request => request.fulfill({ json: {
stats_meta: { groups: Object.fromEntries(['chain', 'node', 'governance', 'market'].map(name =>
[name, { status: 'ready', updated_at: now }])) },
stats: {
mn_stats: { total: '3,123', enabled: '2,139', pose_banned: '984', total_locked: '312,300,000', current_supply: '800,000,000',
coins_percent_locked: '39.04%', roi: '5.20%', roi_one: '7.02%', roi_two: '10.40%' },
blockchain_stats: { version: 50102, connections: '2,329,762' },
superblock_stats: { superblock_date: 'October 30th 2026, 1:00:00 pm', superblock_next_epoch_sec: Math.floor(now / 1000) + 86400 },
price_stats: { price_usd: '0.0019', price_change: '4.04', volume_usd: '56150', market_cap_usd: '1749453', circulating_supply: '800000000' },
}, mapData: {},
} }));
await page.route('**/mncount', request => request.fulfill({ json: [{ date: '2026-10-21', users: 3123 }] }));
await page.goto('/');
await expect(page.getByText('800,000,000 SYS', { exact: true })).toBeVisible();
await expect(page.locator('.chart-canvas canvas')).toHaveCount(1);
await expect(page.locator('.metric-card').filter({ hasText: 'Enabled Sentry Nodes' }).getByText('2,139', { exact: true })).toBeVisible();
await expect(page.locator('.panel--market')).toContainText('SYS Price');
await expect(page.locator('.panel--market')).toContainText('312,300,000 SYS');
await expect(page.locator('.market-exchanges')).toHaveCount(0);
await expect(page.getByRole('link', { name: 'View all markets' })).toHaveCount(0);
await expect(page.locator('a[href*="mexc"], a[href*="kucoin"], a[href*="coinex"], a[href*="bitget"], a[href*="htx"], a[href*="gate.com"]')).toHaveCount(0);
await expect(page.getByRole('link', { name: 'View Proposals', exact: true })).toHaveAttribute('href', '/governance');
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width + 1);
await page.screenshot({ path: testInfo.outputPath(`home-no-exchanges-${width}.png`), fullPage: true });
await page.setViewportSize({ width, height: 1000 });
await page.locator('.panel--market').evaluate(element => window.scrollTo(0, window.scrollY + element.getBoundingClientRect().top - 100));
await page.locator('.panel--market').screenshot({ path: testInfo.outputPath(`market-no-exchanges-${width}.png`) });
});
}
Loading