diff --git a/package.json b/package.json index 930fc00..44cc323 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "postinstall": "patch-package", "test": "npm run test:integration && npm run test:unit", "test:integration": "playwright test", + "test:screenshots": "playwright test -c playwright.screenshot.config.ts", "test:unit": "vitest" }, "devDependencies": { diff --git a/playwright.config.ts b/playwright.config.ts index d60d61c..bf148ce 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,7 +10,7 @@ const config: PlaywrightTestConfig = { port: 4173 }, reporter: process.env.CI ? 'github' : 'list', - testDir: 'tests', + testDir: 'tests/playwright', testMatch: /(.+\.)?(test|spec)\.[jt]s/ }; diff --git a/playwright.screenshot.config.ts b/playwright.screenshot.config.ts new file mode 100644 index 0000000..2eadd5f --- /dev/null +++ b/playwright.screenshot.config.ts @@ -0,0 +1,36 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + use: { + locale: 'en-GB', + timezoneId: 'Europe/Amsterdam' + }, + webServer: { + command: 'npm run build && npm run preview', + port: 4173 + }, + testDir: './tests/screenshots', + outputDir: './test-results/screenshots', + projects: [ + { + name: 'MacBook Air 13 inch', + use: { + viewport: { width: 1440, height: 900 } + } + }, + { + name: 'iPhone 14 Pro', + use: { ...devices['iPhone 14 Pro'] } + }, + { + name: 'iPhone 15 Pro Landscape', + use: { ...devices['iPhone 15 Pro Landscape'] } + }, + { + name: 'MacBook Pro 14 inch', + use: { + viewport: { width: 1512, height: 982 } + } + } + ] +}); diff --git a/src/lib/style/app.scss b/src/lib/style/app.scss index 0fd0702..002e5db 100644 --- a/src/lib/style/app.scss +++ b/src/lib/style/app.scss @@ -53,6 +53,23 @@ nav { .btn-group-sm .btn { font-size: 0.8rem; + // text-overflow: ellipsis; + // white-space: nowrap; + // overflow: hidden; + // width: 4rem; +} + +.btn-group-sm { + display: flex !important; + flex-wrap: wrap !important; + gap: 0.25rem !important; +} + +/* Remove the border radius override that Bootstrap applies */ +.btn-group-sm > .btn { + border-radius: 0.25rem !important; + margin: 0 !important; + position: relative !important; } #customText { @@ -63,7 +80,7 @@ nav { .btclock { background: #000; display: flex; - font-size: calc(3vw + 3vh); + font-size: calc(2vw + 2vh); font-family: 'Antonio', sans-serif; font-weight: 400; padding: 10px; @@ -104,7 +121,7 @@ nav { flex-direction: column; /* Stack the text and line vertically */ align-items: center; justify-content: space-around; /* Distribute items with space between */ - padding: 10px; + padding: 5px; } .splitText div:first-child::after { @@ -116,7 +133,7 @@ nav { } .splitText { - font-size: calc(0.5vw + 1vh); + font-size: calc(0.3vw + 1vh); .top-text, .bottom-text { diff --git a/src/routes/FirmwareUpdater.svelte b/src/routes/FirmwareUpdater.svelte index 2f467c4..a9a3229 100644 --- a/src/routes/FirmwareUpdater.svelte +++ b/src/routes/FirmwareUpdater.svelte @@ -200,7 +200,7 @@

Loading...

{/if}
-
+
Update firmware
-
+
{ - 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.beforeEach(initMock); test('index page has expected columns control, status, settings', async ({ page }) => { await page.goto('/'); @@ -181,6 +74,8 @@ test('time values can not be zero or negative', async ({ page }) => { }); test('info message when fetch eur price is enabled', async ({ page }) => { + delete (settingsJson as { actCurrencies?: string[] }).actCurrencies; + await page.goto('/'); await page.getByRole('button', { name: 'Show all' }).click(); diff --git a/tests/screenshots/viewport-screenshots.spec.ts b/tests/screenshots/viewport-screenshots.spec.ts new file mode 100644 index 0000000..9628342 --- /dev/null +++ b/tests/screenshots/viewport-screenshots.spec.ts @@ -0,0 +1,77 @@ +import { test, expect } from '@playwright/test'; + +import { initMock, settingsJson } from '../shared'; + +test.beforeEach(initMock); + +test('capture screenshots across devices', 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(); + + await page.screenshot({ + path: `./test-results/screenshots/default-${test.info().project.name}.png`, + fullPage: true + }); +}); + +test('capture screenshots across devices with bitaxe screens', async ({ page }) => { + settingsJson.screens = [ + { + id: 0, + name: 'Block Height', + enabled: true + }, + { + id: 3, + name: 'Time', + enabled: true + }, + { + id: 4, + name: 'Halving countdown', + enabled: true + }, + { + id: 6, + name: 'Block Fee Rate', + enabled: true + }, + { + id: 10, + name: 'Sats per dollar', + enabled: true + }, + { + id: 20, + name: 'Ticker', + enabled: true + }, + { + id: 30, + name: 'Market Cap', + enabled: true + }, + { + id: 80, + name: 'BitAxe Hashrate', + enabled: true + }, + { + id: 81, + name: 'BitAxe Best Difficulty', + enabled: true + } + ]; + + 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(); + + await page.screenshot({ + path: `./test-results/screenshots/bitaxe-${test.info().project.name}.png`, + fullPage: true + }); +}); diff --git a/tests/shared.ts b/tests/shared.ts new file mode 100644 index 0000000..18e7e9d --- /dev/null +++ b/tests/shared.ts @@ -0,0 +1,140 @@ +export 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' } + ] +}; + +export 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, + hwRev: 'REV_A_EPD_2_13', + fsRev: '4c5d9616212b27e3f05c35370f0befcf2c5a04b2', + nostrZapPubkey: 'b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422', + lastBuildTime: '1700666677', + screens: [ + { + id: 0, + name: 'Block Height', + enabled: true + }, + { + id: 3, + name: 'Time', + enabled: true + }, + { + id: 4, + name: 'Halving countdown', + enabled: true + }, + { + id: 6, + name: 'Block Fee Rate', + enabled: true + }, + { + id: 10, + name: 'Sats per dollar', + enabled: true + }, + { + id: 20, + name: 'Ticker', + enabled: true + }, + { + id: 30, + name: 'Market Cap', + enabled: true + } + ], + actCurrencies: ['USD', 'EUR'], + availableCurrencies: ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD'] +}; + +export const initMock = 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` + }); + }); +};