diff --git a/src/__tests__/admin-store.test.ts b/src/__tests__/admin-store.test.ts new file mode 100644 index 0000000..1c5f83a --- /dev/null +++ b/src/__tests__/admin-store.test.ts @@ -0,0 +1,34 @@ +import reducer, { + initialState, + filterOrdersByState, + selectOrders, + } from '../store/admin-store'; + + describe('adminStore reducer', () => { + it('should update the filter state', () => { + const nextState = reducer(initialState, filterOrdersByState(1)); + expect(nextState.orders.value.filters.state).toBe(1); + }); + + + it('should return all orders when filter is undefined', () => { + const mockState = { + btAdmin: { + ...initialState, + orders: { + ...initialState.orders, + value: { + list: [ + { state: 1, id: 'a' }, + { state: 2, id: 'b' } + ], + filters: { state: undefined } + } + } + } + }; + const allOrders = selectOrders(mockState as any); + expect(allOrders).toHaveLength(2); + }); + }); + \ No newline at end of file diff --git a/src/__tests__/helpers.test.ts b/src/__tests__/helpers.test.ts new file mode 100644 index 0000000..a6fc966 --- /dev/null +++ b/src/__tests__/helpers.test.ts @@ -0,0 +1,23 @@ +import { clipCenter, numberWithSpaces, orderExpiryFormat } from '../utils/helpers'; + +describe('clipCenter', () => { + it('returns empty string for empty input', () => { + expect(clipCenter('', 10)).toBe(''); + }); + + it('returns full string if shorter than max', () => { + expect(clipCenter('hello', 10)).toBe('hello'); + }); + + it('clips long string in the middle', () => { + expect(clipCenter('abcdefghij1234567890', 10)).toBe('abcde...67890'); + }); +}); + +describe('numberWithSpaces', () => { + it('formats small numbers', () => { + expect(numberWithSpaces(1000)).toBe('1 000'); + expect(numberWithSpaces(1234567)).toBe('1 234 567'); + }); +}); + diff --git a/src/__tests__/public-store.test.ts b/src/__tests__/public-store.test.ts new file mode 100644 index 0000000..d73b55d --- /dev/null +++ b/src/__tests__/public-store.test.ts @@ -0,0 +1,68 @@ +import reducer, { + initialState, + navigate, + setCurrency, + setOrderId, + setShowMenu, + selectCurrentPage, + selectCurrency, + selectCurrentOrderId, + selectShowMenu + } from '../store/public-store'; + + describe('publicStore reducer', () => { + it('should handle navigation updates', () => { + const state = reducer(initialState, navigate({ page: 'payment', orderId: 'abc123' })); + expect(state.navigation.page).toBe('payment'); + expect(state.navigation.orderId).toBe('abc123'); + }); + + it('should set currency', () => { + const state = reducer(initialState, setCurrency('EUR')); + expect(state.settings.currency).toBe('EUR'); + }); + + it('should set order ID', () => { + const state = reducer(initialState, setOrderId('xyz789')); + expect(state.navigation.orderId).toBe('xyz789'); + }); + + it('should toggle menu visibility', () => { + const state = reducer(initialState, setShowMenu(true)); + expect(state.navigation.showMenu).toBe(true); + }); + }); + + describe('publicStore selectors', () => { + const mockState = { + bt: { + ...initialState, + navigation: { + ...initialState.navigation, + page: 'confirm', + orderId: 'order42', + showMenu: true + }, + settings: { + currency: 'JPY' + } + } + }; + + it('should select the current page', () => { + expect(selectCurrentPage(mockState as any)).toBe('confirm'); + }); + + it('should select the currency', () => { + expect(selectCurrency(mockState as any)).toBe('JPY'); + }); + + it('should select the order ID', () => { + expect(selectCurrentOrderId(mockState as any)).toBe('order42'); + }); + + it('should select showMenu as true', () => { + expect(selectShowMenu(mockState as any)).toBe(true); + }); + }); + \ No newline at end of file diff --git a/ui-tests/.github/workflows/playwright.yml b/ui-tests/.github/workflows/playwright.yml new file mode 100644 index 0000000..3eb1314 --- /dev/null +++ b/ui-tests/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/ui-tests/.gitignore b/ui-tests/.gitignore new file mode 100644 index 0000000..58786aa --- /dev/null +++ b/ui-tests/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/ui-tests/package-lock.json b/ui-tests/package-lock.json new file mode 100644 index 0000000..dad61bc --- /dev/null +++ b/ui-tests/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "ui-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ui-tests", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.29" + } + }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.15.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", + "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/ui-tests/package.json b/ui-tests/package.json new file mode 100644 index 0000000..8eb2e74 --- /dev/null +++ b/ui-tests/package.json @@ -0,0 +1,14 @@ +{ + "name": "ui-tests", + "version": "1.0.0", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.29" + } +} diff --git a/ui-tests/playwright.config.ts b/ui-tests/playwright.config.ts new file mode 100644 index 0000000..7c1bab7 --- /dev/null +++ b/ui-tests/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/ui-tests/tests/blocktank-widget.spec.ts b/ui-tests/tests/blocktank-widget.spec.ts new file mode 100644 index 0000000..1322315 --- /dev/null +++ b/ui-tests/tests/blocktank-widget.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from '@playwright/test'; +import { HomePage } from './pages/home-page'; +import { ReviewChannelPage } from './pages/review-page'; + +test('Widget loads', async ({ page }) => { + const widgetPage = new HomePage(page); + + await widgetPage.goto(); + await widgetPage.isLoaded(); +}); + + +test('Create channel flow', async ({ page }) => { + const homePage = new HomePage(page); + await homePage.goto(); + + expect(await homePage.isLoaded()).toBeTruthy(); + + await homePage.fillChannelDetails('1000000', '500000', '52'); + await homePage.enablePrivateChannel(); + await homePage.submit(); + + const reviewPage = new ReviewChannelPage(page); + expect(await reviewPage.isLoaded()); + +}); diff --git a/ui-tests/tests/pages/home-page.ts b/ui-tests/tests/pages/home-page.ts new file mode 100644 index 0000000..5e719cf --- /dev/null +++ b/ui-tests/tests/pages/home-page.ts @@ -0,0 +1,43 @@ +import { Page, Locator } from '@playwright/test'; + +export class HomePage { + + heading: Locator; + receivingInput: Locator; + spendingInput: Locator; + expiryInput: Locator; + privateChannelCheckbox: Locator; + createChannelButton: Locator; + + constructor(private page: Page) { + this.heading = page.locator('.page-heading >> text=My channel'); + this.receivingInput = this.page.locator('#remote-balance'); + this.spendingInput = this.page.locator('#local-balance'); + this.expiryInput = this.page.locator('#channel-expiry'); + this.privateChannelCheckbox = this.page.locator('.custom-checkbox'); + this.createChannelButton = this.page.getByRole('button', { name: 'Create my channel' }); + } + + async goto() { + await this.page.goto('https://widget.synonym.to/?embed=true'); + } + + async isLoaded() { + return await this.heading.isVisible(); + } + + async fillChannelDetails(receiving: string, spending: string, weeks: string) { + await this.receivingInput.fill(receiving); + await this.spendingInput.fill(spending); + await this.expiryInput.fill(weeks); + } + + async enablePrivateChannel() { + await this.privateChannelCheckbox.click(); + } + + async submit() { + await this.createChannelButton.click(); + } + } + \ No newline at end of file diff --git a/ui-tests/tests/pages/review-page.ts b/ui-tests/tests/pages/review-page.ts new file mode 100644 index 0000000..b24fc6e --- /dev/null +++ b/ui-tests/tests/pages/review-page.ts @@ -0,0 +1,15 @@ +import { Page, Locator } from '@playwright/test'; + +export class ReviewChannelPage { + readonly page: Page; + readonly reviewHeading: Locator; + + constructor(page: Page) { + this.page = page; + this.reviewHeading = page.locator('.page-heading >> text=Review Channel'); + } + + async isLoaded() { + return await this.reviewHeading.isVisible(); + } +} \ No newline at end of file