import { expect, test } from '@playwright/test'; const statusJson = { currentScreen: 0, numScreens: 7, timerRunning: true, espUptime: 4479, espFreeHeap: 58508, espHeapSize: 342108, connectionStatus: { price: true, blocks: true }, rssi: -66, data: ['BLOCK/HEIGHT', '8', '1', '8', '0', '2', '6'], rendered: ['BLOCK/HEIGHT', '8', '1', '8', '0', '2', '6'], leds: [ { red: 0, green: 0, blue: 0, hex: '#000000' }, { red: 0, green: 0, blue: 0, hex: '#000000' }, { red: 0, green: 0, blue: 0, hex: '#000000' }, { red: 0, green: 0, blue: 0, hex: '#000000' } ] }; const settingsJson = { numScreens: 7, fgColor: 415029, bgColor: 0, timerSeconds: 1800, timerRunning: true, minSecPriceUpd: 30, fullRefreshMin: 60, wpTimeout: 600, tzOffset: 0, useBitcoinNode: false, mempoolInstance: 'mempool.space', ledTestOnPower: true, ledFlashOnUpd: true, ledBrightness: 128, stealFocus: true, mcapBigChar: true, mdnsEnabled: true, otaEnabled: true, fetchEurPrice: false, hostnamePrefix: 'btclock', hostname: 'btclock-d60b14', ip: '192.168.20.231', txPower: 78, gitRev: '25d8b92bcbc8938417c140355ea3ba99ff9eb4b7', gitTag: '3.1.9', bitaxeEnabled: false, bitaxeHostname: 'bitaxe1', nostrZapNotify: true, mqttEnabled: false, hwRev: 'REV_A_EPD_2_13', fsRev: '4c5d9616212b27e3f05c35370f0befcf2c5a04b2', nostrZapPubkey: 'b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422', lastBuildTime: '1700666677', screens: [ { id: 0, name: 'Block Height', enabled: true }, { id: 1, name: 'Sats per dollar', enabled: true }, { id: 2, name: 'Ticker', enabled: true }, { id: 3, name: 'Time', enabled: true }, { id: 4, name: 'Halving countdown', enabled: true }, { id: 5, name: 'Market Cap', enabled: true } ] }; test.beforeEach(async ({ page }) => { await page.route('*/**/api/status', async (route) => { await route.fulfill({ json: statusJson }); }); await page.route('*/**/api/show/screen/1', async (route) => { //if (route.request().url().includes('*/**/api/show/screen/1')) { statusJson.currentScreen = 1; statusJson.data = ['MSCW/TIME', ' ', ' ', '2', '6', '4', '4']; statusJson.rendered = statusJson.data; //} await route.fulfill({ json: statusJson }); }); await page.route('*/**/api/show/screen/2', async (route) => { statusJson.currentScreen = 2; statusJson.data = ['BTC/USD', '$', '3', '7', '8', '2', '4']; statusJson.rendered = statusJson.data; await route.fulfill({ json: statusJson }); }); await page.route('*/**/api/show/screen/4', async (route) => { statusJson.currentScreen = 4; statusJson.data = ['BIT/COIN', 'HALV/ING', '0/YRS', '149/DAYS', '8/HRS', '30/MINS', 'TO/GO']; statusJson.rendered = statusJson.data; await route.fulfill({ json: statusJson }); }); await page.route('*/**/api/settings', async (route) => { await route.fulfill({ json: settingsJson }); }); await page.route('**/events', (route) => { const newStatus = statusJson; newStatus.data = ['BLOCK/HEIGHT', '8', '0', '0', '8', '1', '5']; // Respond with a custom SSE message route.fulfill({ status: 200, contentType: 'text/event-stream', json: `${JSON.stringify(newStatus)}\n\n` }); }); }); test('index page has expected columns control, status, settings', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('heading', { name: 'Control' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Status' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible(); }); test('index page has working language selector', async ({ page }) => { await page.goto('/'); await expect(page.locator('//*[@id="nav-language-dropdown"]/a')).toBeVisible(); page.locator('//*[@id="nav-language-dropdown"]/a').click(); //*[@id="nav-language-dropdown"]/ul/li[1]/button await expect(page.locator('//*[@id="nav-language-dropdown"]/ul/li[1]/button')).toBeVisible(); page.locator('//*[@id="nav-language-dropdown"]/ul/li[2]/button').click(); await expect(page.getByRole('heading', { name: 'Instellingen' })).toBeVisible(); page.locator('//*[@id="nav-language-dropdown"]/a').click(); page.locator('//*[@id="nav-language-dropdown"]/ul/li[3]/button').click(); await expect(page.getByRole('heading', { name: 'ConfiguraciĆ³n' })).toBeVisible(); }); test('api page has expected load button', async ({ page }) => { await page.goto('/api'); await expect(page.getByRole('button', { name: 'Load' })).toBeVisible(); }); test('timezone can be negative, zero and positive', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: 'Show all' }).click(); const tzOffsetField = 'input#tzOffset'; for (const val of ['-10', '0', '42']) { await page.fill(tzOffsetField, val); const resultValue = await page.$eval(tzOffsetField, (input: HTMLInputElement) => input.value); expect(resultValue).toBe(val); await page.getByRole('button', { name: 'Save' }).click(); } }); test('time values can not be zero or negative', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: 'Show all' }).click(); for (const field of ['#timePerScreen', '#fullRefreshMin', '#minSecPriceUpd']) { for (const val of ['42', '210']) { await page.fill(field, val); const resultValue = await page.$eval(field, (input: HTMLInputElement) => input.value); expect(resultValue).toBe(val); await page.getByRole('button', { name: 'Save' }).click(); const validationMessage = await page.$eval( field, (input: HTMLInputElement) => input.validationMessage ); expect(validationMessage).not.toContain('Value must be greater'); } for (const val of ['-10', '0']) { await page.fill(field, val); const resultValue = await page.$eval(field, (input: HTMLInputElement) => input.value); expect(resultValue).toBe(val); await page.getByRole('button', { name: 'Save' }).click(); const validationMessage = await page.$eval( field, (input: HTMLInputElement) => input.validationMessage ); expect(validationMessage).toContain('Value must be greater'); } } }); test('info message when fetch eur price is enabled', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: 'Show all' }).click(); const inputField = 'input#fetchEurPrice'; const switchElement = await page.locator(inputField); expect(switchElement).toBeTruthy(); const isSwitchEnabled = await switchElement.isChecked(); expect(isSwitchEnabled).toBe(false); await expect(page.getByText('the WS Price connection will show')).toBeHidden(); await switchElement.click(); const isSwitchNowEnabled = await switchElement.isChecked(); expect(isSwitchNowEnabled).toBe(true); await expect(page.getByText('the WS Price connection will show')).toBeVisible(); }); test('npub values will be converted to hex pubkeys', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: 'Show all' }).click(); for (const field of ['#nostrZapPubkey']) { for (const val of ['npub1k5f85zx0xdskyayqpfpc0zq6n7vwqjuuxugkayk72fgynp34cs3qfcvqg2']) { await page.fill(field, val); await page.getByLabel('Nostr Relay').click(); const resultValue = await page.$eval(field, (input: HTMLInputElement) => input.value); expect(resultValue).toBe('b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422'); } } }); test('empty nostr relay field is not accepted', async ({ page }) => { await page.goto('/'); await page.getByRole('button', { name: 'Show all' }).click(); const nostrRelayField = page.getByLabel('Nostr Relay'); nostrRelayField.fill(''); await page.getByRole('button', { name: 'Save' }).click(); const validationMessage = await nostrRelayField.evaluate((el) => el.validationMessage); expect(validationMessage).toContain('Please fill out this field'); }); test('screens should be able to change', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('button', { name: 'Sats per Dollar' })).toBeVisible(); const responsePromise = page.waitForRequest('*/**/api/show/screen/*'); await page.getByRole('button', { name: 'Sats per Dollar' }).click(); const response = await responsePromise; expect(response.url()).toContain('api/show/screen/1'); }); test('parse all types of EPD content correctly', async ({ page }) => { statusJson.data[2] = '123'; await page.route('**/events', (route) => { const newStatus = statusJson; newStatus.data = ['BLOCK/HEIGHT', '8', '123', '0', '8', '1', '5']; // Respond with a custom SSE message route.fulfill({ status: 200, contentType: 'text/event-stream', json: `${JSON.stringify(newStatus)}\n\n` }); }); await page.goto('/'); await expect(page.getByRole('heading', { name: 'Status' })).toBeVisible(); await page.waitForSelector('#timerStatusText:has-text("running")'); await page.waitForSelector('#btclock-wrapper > div > div:nth-child(1)'); expect(statusJson.data[0]).toContain('/'); await expect(page.locator('#btclock-wrapper > div > div:nth-child(1)')).toBeTruthy(); await expect(page.locator('#btclock-wrapper > div > div:nth-child(1)')).toHaveClass('splitText'); expect(statusJson.data[1]).toHaveLength(1); await expect(page.locator('#btclock-wrapper > div > div:nth-child(2)')).toHaveClass('digit'); expect(statusJson.data[2]).toHaveLength(3); await expect(page.locator('#btclock-wrapper > div > div:nth-child(3)')).toHaveClass('mediumText'); }); test('should work with more than 7 screens', async ({ page }) => { statusJson.data[2] = '1'; statusJson.numScreens = 9; settingsJson.numScreens = 9; statusJson.data.splice(1, 0, ' ', ' '); await page.goto('/'); await expect(page.getByRole('heading', { name: 'Status' })).toBeVisible(); await page.waitForSelector('#timerStatusText:has-text("running")'); await expect(page.locator('#btclock-wrapper > div > div:nth-child(9)')).toBeTruthy(); await expect(page.locator('#customText')).toHaveAttribute( 'maxlength', statusJson.numScreens.toString() ); });