diff --git a/README.md b/README.md index e8fe243..b125d37 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/components/ExchangeGrid.js b/src/components/ExchangeGrid.js index 7680677..d552a57 100644 --- a/src/components/ExchangeGrid.js +++ b/src/components/ExchangeGrid.js @@ -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 (
diff --git a/src/components/ExchangeGrid.test.js b/src/components/ExchangeGrid.test.js new file mode 100644 index 0000000..414a662 --- /dev/null +++ b/src/components/ExchangeGrid.test.js @@ -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(); + 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(); + 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(); +}); diff --git a/src/data/exchanges.js b/src/data/exchanges.js index a3c70d6..f65fc9a 100644 --- a/src/data/exchanges.js +++ b/src/data/exchanges.js @@ -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; diff --git a/src/pages/NetworkAvailability.test.js b/src/pages/NetworkAvailability.test.js index 1b7e81a..6181854 100644 --- a/src/pages/NetworkAvailability.test.js +++ b/src/pages/NetworkAvailability.test.js @@ -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(); + 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(); +}); diff --git a/tests/browser/network-availability.spec.cjs b/tests/browser/network-availability.spec.cjs index 51db1c6..30b3642 100644 --- a/tests/browser/network-availability.spec.cjs +++ b/tests/browser/network-availability.spec.cjs @@ -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`) }); + }); +}