diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..467190b --- /dev/null +++ b/.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: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 6c255e3..9cabfab 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,7 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts next-env.d.ts +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/export-dashboard.spec.ts b/e2e/export-dashboard.spec.ts new file mode 100644 index 0000000..6373c0c --- /dev/null +++ b/e2e/export-dashboard.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from "@playwright/test"; + +const devURL = "http://localhost:3000/"; + +test.describe("Dashboard", () => { + test("has export button", async ({ page }) => { + await page.goto(devURL); + + await page.getByRole("link", { name: /dashboard/i }).click(); + await expect( + page.getByRole("button", { name: "Export (PNG)" }) + ).toBeVisible(); + }); + + test("has name of Sprout pool plus amount of zec", async ({ page }) => { + await page.goto(devURL); + + await page.getByRole("link", { name: /dashboard/i }).click(); + + await page.getByRole("button", { name: "Sprout Pool" }).click(); + + await page.getByRole("button", { name: "Export (PNG)" }).click(); + + await expect(page.getByText(/ZEC in Sprout Pool/i)).toBeVisible(); + }); + + test("has name of Sapling pool plus amount of zec", async ({ page }) => { + await page.goto(devURL); + + await page.getByRole("link", { name: /dashboard/i }).click(); + + await page.getByRole("button", { name: "Sapling Pool" }).click(); + + await page.getByRole("button", { name: "Export (PNG)" }).click(); + + await expect(page.getByText(/ZEC in Sapling Pool/i)).toBeVisible(); + }); + + test("has name of Orchard pool plus amount of zec", async ({ page }) => { + await page.goto(devURL); + + await page.getByRole("link", { name: /dashboard/i }).click(); + + await page.getByRole("button", { name: "Orchard Pool" }).click(); + + await page.getByRole("button", { name: "Export (PNG)" }).click(); + + await expect(page.getByText(/ZEC in Orchard Pool/i)).toBeVisible(); + }); +}); diff --git a/jest-setup.js b/jest-setup.js new file mode 100644 index 0000000..d0de870 --- /dev/null +++ b/jest-setup.js @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..0949d32 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,18 @@ +const nextJest = require("next/jest"); + +const createJestConfig = nextJest({ dir: "./" }); + +/** @type {import('jest').Config}*/ +const customJestConfig = { + setupFilesAfterEnv: ["/jest-setup.js"], + moduleDirectories: ["node_modules", "/"], + testPathIgnorePatterns: ["/e2e"], // ignores e2e + testEnvironment: "jest-environment-jsdom", + collectCoverageFrom: [ + "**/*.{js,jsx}", + "!**/node_modules/**", + "!**/vendor/**", + ], +}; + +module.exports = createJestConfig(customJestConfig); diff --git a/package-lock.json b/package-lock.json index 5d8f435..c1e344f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ }, "devDependencies": { "@octokit/types": "^11.1.0", + "@playwright/test": "^1.45.1", "@types/marked": "^5.0.1", "@types/node": "20.5.7", "@types/react": "18.2.21", @@ -1151,6 +1152,21 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@playwright/test": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", + "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", + "dev": true, + "dependencies": { + "playwright": "1.45.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -7088,6 +7104,50 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", + "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "dev": true, + "dependencies": { + "playwright-core": "1.45.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", + "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/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, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", diff --git a/package.json b/package.json index 0be18a7..d407ab9 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,11 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test:coverage": "jest --coverage", + "test": "jest --watch", + "test:e2e": "npx playwright test", + "test:e2e:ui": "npx playwright test --ui" }, "dependencies": { "@headlessui/react": "^1.7.18", @@ -36,6 +40,7 @@ "flowbite-react": "^0.6.4", "framer-motion": "^11.0.3", "gitlog": "^4.0.8", + "html2canvas": "^1.4.1", "mongodb": "6.3", "next": "^13.5.6", "next-auth": "^4.24.5", @@ -62,6 +67,13 @@ }, "devDependencies": { "@octokit/types": "^11.1.0", + "@playwright/test": "^1.45.1", + "@testing-library/dom": "^10.3.1", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", "@types/marked": "^5.0.1", "@types/node": "20.5.7", "@types/react": "18.2.21", @@ -69,6 +81,8 @@ "@types/react-icons": "^3.0.0", "@types/serviceworker": "^0.0.82", "@types/web-push": "^3.6.3", - "eslint-config-prettier": "^9.1.0" + "eslint-config-prettier": "^9.1.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..9a85fef --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e", + /* 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://127.0.0.1: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://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/src/app/dashboard/ShieldedPoolDashboard.tsx b/src/app/dashboard/ShieldedPoolDashboard.tsx index 463e9f4..ad1c6de 100644 --- a/src/app/dashboard/ShieldedPoolDashboard.tsx +++ b/src/app/dashboard/ShieldedPoolDashboard.tsx @@ -1,8 +1,10 @@ "use client"; -import React, { useState, useEffect } from "react"; -import dynamic from "next/dynamic"; import Button from "@/components/Button/Button"; import HalvingMeter from "@/components/HalvingMeter"; +import Tools from "@/components/tools"; +import useExportDashboardAsPNG from "@/hooks/useExportDashboardAsPNG"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; const ShieldedPoolChart = dynamic( () => import("../../components/ShieldedPoolChart"), @@ -20,7 +22,8 @@ const orchardUrl = const apiUrl = "https://api.github.com/repos/ZecHub/zechub-wiki/commits?path=public/data/shielded_supply.json"; -const blockchainInfoUrl = "https://mainnet.zcashexplorer.app/api/v1/blockchain-info"; +const blockchainInfoUrl = + "https://mainnet.zcashexplorer.app/api/v1/blockchain-info"; interface BlockchainInfo { blocks: number; @@ -95,17 +98,25 @@ const ShieldedPoolDashboard = () => { const [orchardSupply, setOrchardSupply] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); + const { divChartRef, handleSaveToPng } = useExportDashboardAsPNG(); + useEffect(() => { getBlockchainData().then((data) => setBlockchainInfo(data)); getBlockchainInfo().then((data) => setCirculation(data)); getLastUpdatedDate().then((date) => setLastUpdated(date.split("T")[0])); - - getSupplyData(sproutUrl).then((data) => setSproutSupply(data[data.length - 1])); - - getSupplyData(saplingUrl).then((data) => setSaplingSupply(data[data.length - 1])); - - getSupplyData(orchardUrl).then((data) => setOrchardSupply(data[data.length - 1])); + + getSupplyData(sproutUrl).then((data) => + setSproutSupply(data[data.length - 1]) + ); + + getSupplyData(saplingUrl).then((data) => + setSaplingSupply(data[data.length - 1]) + ); + + getSupplyData(orchardUrl).then((data) => + setOrchardSupply(data[data.length - 1]) + ); }, []); useEffect(() => { @@ -152,13 +163,30 @@ const ShieldedPoolDashboard = () => {

Shielded Supply Chart (ZEC)

+
- +
+ +
-
- - Last updated: {lastUpdated ? new Date(lastUpdated).toLocaleDateString() : "Loading..."} +
+ + Last updated:{" "} + {lastUpdated + ? new Date(lastUpdated).toLocaleDateString() + : "Loading..."} +
@@ -181,7 +209,9 @@ const ShieldedPoolDashboard = () => { }`} /> - {sproutSupply ? `${sproutSupply.supply.toLocaleString()} ZEC` : "Loading..."} + {sproutSupply + ? `${sproutSupply.supply.toLocaleString()} ZEC` + : "Loading..."}
@@ -193,7 +223,9 @@ const ShieldedPoolDashboard = () => { }`} /> - {saplingSupply ? `${saplingSupply.supply.toLocaleString()} ZEC` : "Loading..."} + {saplingSupply + ? `${saplingSupply.supply.toLocaleString()} ZEC` + : "Loading..."}
@@ -205,7 +237,9 @@ const ShieldedPoolDashboard = () => { }`} /> - {orchardSupply ? `${orchardSupply.supply.toLocaleString()} ZEC` : "Loading..."} + {orchardSupply + ? `${orchardSupply.supply.toLocaleString()} ZEC` + : "Loading..."}
@@ -263,7 +297,10 @@ const ShieldedPoolDashboard = () => { Circulation - {circulation !== null ? `${circulation.toLocaleString()}` : "N/A"} ZEC + {circulation !== null + ? `${circulation.toLocaleString()}` + : "N/A"}{" "} + ZEC diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 9732dc0..f0cbc70 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -5,19 +5,24 @@ interface ButtonProps { text: string; className?: string; onClick?: () => void; + styles?: React.CSSProperties } -const Button: React.FC = ({ href, text, className, onClick }) => { +const Button: React.FC = ({ href, text, className, onClick, styles }) => { if (href) { return ( - + {text} ); } return ( - ); diff --git a/src/components/__tests__/button.test.tsx b/src/components/__tests__/button.test.tsx new file mode 100644 index 0000000..4cd6b22 --- /dev/null +++ b/src/components/__tests__/button.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import Button from "../Button/Button"; + +describe("Button", () => { + const user = userEvent.setup(); + + it("calls onClick prop when clicked", async () => { + const handleClick = jest.fn(); + + render(