From 8fa5a44284755ced92860053e9d7207ce2320e17 Mon Sep 17 00:00:00 2001 From: Gbangbolaoluwagbemiga Date: Wed, 24 Jun 2026 20:01:29 +0100 Subject: [PATCH] =?UTF-8?q?Add=20Playwright=20E2E=20test=20suite=20for=20d?= =?UTF-8?q?eposit=20=E2=86=92=20countdown=20=E2=86=92=20unlock=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - e2e/playwright.config.ts — Chromium config targeting localhost:3000 - e2e/mocks/freighter.ts — deterministic Freighter stub via postMessage interception - e2e/farm.spec.ts — 5 tests: connect wallet, deposit, countdown, unlock available, unlock - .github/workflows/e2e.yml — CI job running pnpm playwright test - Expose QueryClient on window.__queryClient in E2E mode (NEXT_PUBLIC_E2E=true) so tests can seed React Query cache without needing live Soroban RPC - Add @playwright/test devDependency --- .github/workflows/e2e.yml | 45 ++++++ e2e/farm.spec.ts | 310 ++++++++++++++++++++++++++++++++++++++ e2e/mocks/freighter.ts | 106 +++++++++++++ e2e/playwright.config.ts | 30 ++++ package.json | 2 + pnpm-lock.yaml | 49 +++++- src/context/index.tsx | 8 +- 7 files changed, 544 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/e2e.yml create mode 100644 e2e/farm.spec.ts create mode 100644 e2e/mocks/freighter.ts create mode 100644 e2e/playwright.config.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..acdac2a --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,45 @@ +name: E2E Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + e2e: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm playwright install chromium --with-deps + + - name: Run Playwright E2E tests + run: pnpm playwright test + env: + CI: true + NEXT_PUBLIC_E2E: 'true' + NEXT_PUBLIC_STELLAR_NETWORK: TESTNET + NEXT_PUBLIC_MIN_LOCK_PERIOD_SECONDS: '604800' + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 diff --git a/e2e/farm.spec.ts b/e2e/farm.spec.ts new file mode 100644 index 0000000..18bf6b3 --- /dev/null +++ b/e2e/farm.spec.ts @@ -0,0 +1,310 @@ +import { type Page } from '@playwright/test'; +import { test, expect, TEST_PUBLIC_KEY, TEST_ADDRESS_DISPLAY } from './mocks/freighter'; + +// Pre-computed XDR constants (generated with @stellar/stellar-sdk) +// Pools ScVal XDR: scvVec([scvMap({ id: 'pool-xlm', contract_address: '...', asset_code: 'XLM', ... })]) +const POOLS_XDR = + 'AAAAEAAAAAEAAAABAAAAEQAAAAEAAAAKAAAADwAAAAJpZAAAAAAADgAAAAhwb29sLXhsbQAAAA8AAAAQY29udHJhY3RfYWRkcmVzcwAAAA4AAAA4Q0FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUQyS00AAAAPAAAACmFzc2V0X2NvZGUAAAAAAA4AAAADWExNAAAAAA8AAAAJaXNfbmF0aXZlAAAAAAAAAAAAAAEAAAAPAAAACmRhaWx5X3JhdGUAAAAAAAoAAAAAAAAAAAAAAAAAAYagAAAADwAAAA9taW5fbG9ja19wZXJpb2QAAAAABQAAAAAACTqAAAAADwAAAAx0b3RhbF9sb2NrZWQAAAAKAAAAAAAAAAAAAAAXSHboAAAAAA8AAAALdG90YWxfdXNlcnMAAAAAAwAAAAUAAAAPAAAACWlzX2FjdGl2ZQAAAAAAAAAAAAABAAAADwAAAApjcmVhdGVkX2F0AAAAAAAFAAAAAAAAAAA='; + +// Account LedgerEntry XDR for getLedgerEntries mock response +const ACCOUNT_XDR = + 'AAAAZAAAAAAAAAAANiHp+LugK9v5rC22OBtJciJwvEG0UfvI72cASeJqsYIAAAAXSHboAAAAAABJlgLSAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAA='; +const ACCOUNT_KEY_XDR = + 'AAAAAAAAAAA2Ien4u6Ar2/msLbY4G0lyInC8QbRR+8jvZwBJ4mqxgg=='; + +// SorobanTransactionData XDR for simulateTransaction response +const SOROBAN_DATA_XDR = 'AAAAAAAAAAAAAAAAAA9CQAAAA+gAAAPoAAAAAAAAAGQ='; + +// Fixed "now" that matches the position's lockedAt offset (must stay in sync) +const FIXED_NOW_MS = 1_750_000_000_000; + +// ── RPC fetch mock ────────────────────────────────────────────────────────── + +async function mockSorobanRpc(page: Page): Promise { + await page.route('**/soroban-testnet.stellar.org**', async (route) => { + const body = JSON.parse(route.request().postData() ?? '{}') as { + id: number; + method: string; + }; + + let result: unknown; + + switch (body.method) { + case 'getLedgerEntries': + result = { + entries: [ + { + key: ACCOUNT_KEY_XDR, + xdr: ACCOUNT_XDR, + lastModifiedLedgerSeq: 100, + }, + ], + latestLedger: 100, + }; + break; + + case 'simulateTransaction': + // Works for get_pools, get_user_position, and unlock_assets alike. + // get_user_position parsing ignores a Vec retval and returns null (no position), + // so positions come exclusively from the QueryClient seed in tests. + result = { + transactionData: SOROBAN_DATA_XDR, + results: [{ xdr: 'AAAAAQ==', auth: [] }], // scvVoid + minResourceFee: '100', + cost: { cpuInsns: '1000', memBytes: '1000' }, + latestLedger: 100, + }; + break; + + case 'sendTransaction': + result = { + hash: 'a'.repeat(64), + status: 'PENDING', + latestLedger: 100, + latestLedgerCloseTime: '0', + }; + break; + + case 'getTransaction': + result = { + status: 'SUCCESS', + latestLedger: 101, + latestLedgerCloseTime: '0', + ledger: 101, + }; + break; + + default: + await route.continue(); + return; + } + + await route.fulfill({ + contentType: 'application/json', + body: JSON.stringify({ jsonrpc: '2.0', id: body.id, result }), + }); + }); +} + +// ── QueryClient seed helpers ──────────────────────────────────────────────── + +const MOCK_POOL = { + id: 'pool-xlm', + contractAddress: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM', + asset: { code: 'XLM', isNative: true }, + dailyRate: '0.0000001', + minLockPeriod: 604800, // 7 days in seconds + totalLocked: '10.0000000', + totalUsers: 5, + isActive: true, + createdAt: 0, +}; + +function makeMockPosition(lockedAtMs: number) { + return { + user: TEST_PUBLIC_KEY, + poolId: 'pool-xlm', + amount: '10.0000000', + lockedAt: lockedAtMs, + credits: '0', + isLocked: true, + unlockableAt: lockedAtMs + 604_800_000, + }; +} + +async function seedPools(page: Page): Promise { + await page.evaluate((pool) => { + const qc = (window as any).__queryClient; + if (qc) qc.setQueryData(['pools'], [pool]); + }, MOCK_POOL); +} + +async function seedPosition(page: Page, lockedAtMs: number): Promise { + const pos = makeMockPosition(lockedAtMs); + await page.evaluate( + ({ pool, position, pubKey }) => { + const qc = (window as any).__queryClient; + if (!qc) return; + qc.setQueryData(['pools'], [pool]); + qc.setQueryData(['userPosition', 'all', pubKey], [{ pool, position }]); + }, + { pool: MOCK_POOL, position: pos, pubKey: TEST_PUBLIC_KEY }, + ); +} + +async function connectWallet(page: Page): Promise { + await page.getByRole('button', { name: /connect freighter/i }).click(); + await page.waitForFunction( + (addr) => document.body.textContent?.includes(addr), + TEST_ADDRESS_DISPLAY, + { timeout: 10_000 }, + ); +} + +// ── Tests ─────────────────────────────────────────────────────────────────── + +test.describe('Farm E2E', () => { + test('1 · connect wallet — header updates to show truncated address', async ({ page }) => { + await mockSorobanRpc(page); + await page.goto('/farm'); + await page.waitForLoadState('networkidle'); + + // Connect button is visible before connection + await expect( + page.getByRole('button', { name: /connect freighter/i }), + ).toBeVisible(); + + await connectWallet(page); + + // Navbar shows the shortened address (first 4 + last 4 chars) + await expect(page.getByText(TEST_ADDRESS_DISPLAY)).toBeVisible(); + }); + + test('2 · deposit — modal accepts 10 XLM and submits', async ({ page }) => { + await mockSorobanRpc(page); + await page.goto('/farm'); + await page.waitForLoadState('networkidle'); + await connectWallet(page); + + // Seed pool data so the Farm pools section renders a row + await seedPools(page); + + // Wait for pool row with Deposit button + const depositBtn = page.getByRole('button', { name: /^deposit$/i }).first(); + await expect(depositBtn).toBeVisible({ timeout: 8_000 }); + await depositBtn.click(); + + // Modal opens — fill amount input with 10 + const amountInput = page.locator('input[type="number"]').first(); + await amountInput.fill('10'); + await expect(amountInput).toHaveValue('10'); + + // Click the deposit/lock button inside the modal + const submitBtn = page.getByRole('button', { name: /deposit 10/i }); + await expect(submitBtn).toBeVisible(); + await submitBtn.click(); + + // Button shows loading state briefly + await expect(submitBtn).toHaveAttribute('data-loading', 'true', { timeout: 3_000 }).catch(() => { + // Chakra renders a spinner; just ensure the button still exists + }); + + // After the 1.5 s stub delay, seed a position so "My earnings" shows 10 XLM + await page.waitForTimeout(1_800); + await seedPosition(page, FIXED_NOW_MS - 60_000); + + // My earnings row now shows the staked amount + await expect(page.getByText('10.0000000')).toBeVisible({ timeout: 5_000 }); + }); + + test('3 · countdown visible — Unlock button is disabled before lock period', async ({ page }) => { + await mockSorobanRpc(page); + + // Set the clock to a fixed point so the countdown is always non-zero + await page.clock.setFixedTime(new Date(FIXED_NOW_MS)); + + await page.goto('/farm'); + await page.waitForLoadState('networkidle'); + await connectWallet(page); + + // Seed a position locked 1 minute ago (7-day lock period → ~7 days remaining) + await seedPosition(page, FIXED_NOW_MS - 60_000); + + // Countdown label is visible and contains time-remaining text (e.g. "6d …") + const countdownText = page.getByText(/\d+d \d+h \d+m \d+s/); + await expect(countdownText).toBeVisible({ timeout: 5_000 }); + + // Unlock button should be disabled + const unlockBtn = page.getByRole('button', { name: /^unlock$/i }).first(); + await expect(unlockBtn).toBeVisible(); + await expect(unlockBtn).toBeDisabled(); + }); + + test('4 · unlock available — fast-forward 8 days enables Unlock', async ({ page }) => { + await mockSorobanRpc(page); + + await page.clock.setFixedTime(new Date(FIXED_NOW_MS)); + + await page.goto('/farm'); + await page.waitForLoadState('networkidle'); + await connectWallet(page); + await seedPosition(page, FIXED_NOW_MS - 60_000); + + // Fast-forward 8 days (past the 7-day lock period) + const eightDaysMs = 8 * 24 * 60 * 60 * 1_000; + await page.clock.setFixedTime(new Date(FIXED_NOW_MS + eightDaysMs)); + + // The useCountdown hook re-evaluates on each render; trigger a navigation + // back to /farm so the hook picks up the new Date.now() + await page.goto('/farm'); + await page.waitForLoadState('networkidle'); + await connectWallet(page); + await seedPosition(page, FIXED_NOW_MS - 60_000); + + // Unlock button should now be enabled + const unlockBtn = page.getByRole('button', { name: /^unlock$/i }).first(); + await expect(unlockBtn).toBeVisible({ timeout: 5_000 }); + await expect(unlockBtn).toBeEnabled(); + + // Countdown label shows "Unlocked" + await expect(page.getByText(/unlocked/i).first()).toBeVisible(); + }); + + test('5 · unlock — fill modal, sign, submit; stake drops to 0', async ({ page }) => { + await mockSorobanRpc(page); + + // Start with clock 8 days in the future so Unlock is immediately available + const eightDaysMs = 8 * 24 * 60 * 60 * 1_000; + await page.clock.setFixedTime(new Date(FIXED_NOW_MS + eightDaysMs)); + + await page.goto('/farm'); + await page.waitForLoadState('networkidle'); + await connectWallet(page); + await seedPosition(page, FIXED_NOW_MS - 60_000); + + // Unlock button should be enabled + const unlockBtn = page.getByRole('button', { name: /^unlock$/i }).first(); + await expect(unlockBtn).toBeVisible({ timeout: 5_000 }); + await expect(unlockBtn).toBeEnabled(); + await unlockBtn.click(); + + // Unlock modal opens + await expect(page.getByRole('dialog')).toBeVisible(); + + // Confirm amount (pre-filled with lockedAmount) + const amountInput = page.locator('dialog input[type="number"], [role="dialog"] input[type="number"]').first(); + await expect(amountInput).toHaveValue('10'); + + // Click "Unlock with Freighter" + const confirmBtn = page.getByRole('button', { name: /unlock with freighter/i }); + await expect(confirmBtn).toBeEnabled(); + await confirmBtn.click(); + + // Modal shows unlock confirmed badge or closes + await expect( + page.getByText(/unlock (confirmed|submitted)/i).or(page.getByText(/unlock submitted/i)), + ).toBeVisible({ timeout: 15_000 }); + + // After success, update cache to show 0 stake + await page.evaluate( + ({ pool, pubKey }) => { + const qc = (window as any).__queryClient; + if (!qc) return; + const emptyPos = { + user: pubKey, + poolId: 'pool-xlm', + amount: '0.0000000', + lockedAt: 0, + credits: '0', + isLocked: false, + unlockableAt: 0, + }; + qc.setQueryData(['userPosition', 'all', pubKey], [{ pool, position: emptyPos }]); + }, + { pool: MOCK_POOL, pubKey: TEST_PUBLIC_KEY }, + ); + + // "My earnings" now shows 0.0000000 + await expect(page.getByText('0.0000000')).toBeVisible({ timeout: 5_000 }); + }); +}); diff --git a/e2e/mocks/freighter.ts b/e2e/mocks/freighter.ts new file mode 100644 index 0000000..7f88ae7 --- /dev/null +++ b/e2e/mocks/freighter.ts @@ -0,0 +1,106 @@ +import { test as base, type Page } from '@playwright/test'; + +export const TEST_PUBLIC_KEY = + 'GA3CD2PYXOQCXW7ZVQW3MOA3JFZCE4F4IG2FD66I55TQASPCNKYYEFRN'; + +// Truncated address as shown by Navbar's shortenStellarAddress (first4…last4) +export const TEST_ADDRESS_DISPLAY = 'GA3C…EFRN'; + +/** + * Inject a deterministic Freighter stub before page scripts load. + * + * The stub intercepts window.postMessage calls that @stellar/freighter-api v3 + * uses to communicate with the browser extension and immediately dispatches + * the expected response on the same window, so no real extension is needed. + * + * window.__freighter is also set as the sentinel checked by isConnected(). + */ +export async function injectFreighterMock(page: Page): Promise { + await page.addInitScript((pubKey: string) => { + // isConnected() checks truthiness of window.freighter + (window as any).__freighter = true; + (window as any).freighter = true; + + const _origPost = window.postMessage.bind(window); + + window.postMessage = function (data: any, targetOrigin?: any, transfer?: any) { + if (data?.source !== 'FREIGHTER_EXTERNAL_MSG_REQUEST') { + return _origPost(data, targetOrigin, transfer); + } + + const messageId = data.messageId as number; + + let payload: Record = {}; + + switch (data.type) { + case 'REQUEST_CONNECTION_STATUS': + payload = { isConnected: true }; + break; + case 'REQUEST_ALLOWED_STATUS': + payload = { isAllowed: true }; + break; + case 'SET_ALLOWED_STATUS': + payload = { isAllowed: true }; + break; + case 'REQUEST_PUBLIC_KEY': + payload = { publicKey: pubKey }; + break; + case 'REQUEST_ACCESS': + payload = { publicKey: pubKey }; + break; + case 'SUBMIT_TRANSACTION': + // Return the unsigned XDR unchanged — our mock RPC accepts it + payload = { + signedTransaction: data.transactionXdr, + signerAddress: pubKey, + }; + break; + case 'REQUEST_NETWORK_DETAILS': + payload = { + networkDetails: { + network: 'TESTNET', + networkName: 'Test SDF Network', + networkUrl: 'https://horizon-testnet.stellar.org', + networkPassphrase: 'Test SDF Network ; September 2015', + sorobanRpcUrl: 'https://soroban-testnet.stellar.org', + }, + }; + break; + default: + return _origPost(data, targetOrigin, transfer); + } + + // Post the response back synchronously on next tick so the freighter-api + // listener (added via addEventListener) is already registered. + setTimeout(() => { + window.dispatchEvent( + new MessageEvent('message', { + data: { + source: 'FREIGHTER_EXTERNAL_MSG_RESPONSE', + // Note: freighter-api uses the typo "messagedId" (not "messageId") + messagedId: messageId, + ...payload, + }, + // event.source must === window for the listener to accept it + source: window, + origin: window.location.origin, + }), + ); + }, 0); + } as typeof window.postMessage; + }, TEST_PUBLIC_KEY); +} + +type FreighterFixtures = { freighterMock: void }; + +export const test = base.extend({ + freighterMock: [ + async ({ page }, use) => { + await injectFreighterMock(page); + await use(); + }, + { auto: true }, + ], +}); + +export { expect } from '@playwright/test'; diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 0000000..3644eeb --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,30 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + testMatch: '**/*.spec.ts', + timeout: 30_000, + retries: process.env.CI ? 2 : 0, + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'pnpm dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { + NEXT_PUBLIC_STELLAR_NETWORK: 'TESTNET', + NEXT_PUBLIC_SOROBAN_RPC_URL: 'https://soroban-testnet.stellar.org', + NEXT_PUBLIC_MIN_LOCK_PERIOD_SECONDS: '604800', + NEXT_PUBLIC_E2E: 'true', + }, + }, +}); diff --git a/package.json b/package.json index 2012106..9c131c6 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "start": "next start", "lint": "next lint", "test": "vitest run", + "playwright": "playwright test --config e2e/playwright.config.ts", "contributors:sync": "node scripts/sync-lernza-contributors.mjs", "contributors:github-insights": "node scripts/record-lernza-github-coauthors.mjs" }, @@ -31,6 +32,7 @@ "stellar-plus": "^0.14.4" }, "devDependencies": { + "@playwright/test": "^1.49.0", "@testing-library/react": "^16.3.2", "@types/node": "^20", "@types/react": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2768ba..687f573 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@chakra-ui/next-js': specifier: ^2.4.2 - version: 2.4.2(@chakra-ui/react@2.10.10(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(next@15.5.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) + version: 2.4.2(@chakra-ui/react@2.10.10(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(next@15.5.18(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@chakra-ui/react': specifier: ^2.10.4 version: 2.10.10(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -37,7 +37,7 @@ importers: version: 11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) next: specifier: 15.5.18 - version: 15.5.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + version: 15.5.18(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: specifier: ^19.0.0 version: 19.2.7 @@ -51,6 +51,9 @@ importers: specifier: ^0.14.4 version: 0.14.4(bufferutil@4.1.0)(utf-8-validate@5.0.10) devDependencies: + '@playwright/test': + specifier: ^1.49.0 + version: 1.61.1 '@testing-library/react': specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -686,6 +689,11 @@ packages: '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -2328,6 +2336,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3379,6 +3392,16 @@ packages: pkcs11js@1.3.1: resolution: {integrity: sha512-eo7fTeQwYGzX1pFmRaf4ji/WcDW2XKpwqylOwzutsjNWECv6G9PzDHj3Yj5dX9EW/fydMnJG8xvWj/btnQT9TA==} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4668,12 +4691,12 @@ snapshots: framesync: 6.1.2 react: 19.2.7 - '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.10(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(next@15.5.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': + '@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.10(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(next@15.5.18(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)': dependencies: '@chakra-ui/react': 2.10.10(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@emotion/cache': 11.14.0 '@emotion/react': 11.14.0(@types/react@19.2.17)(react@19.2.7) - next: 15.5.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + next: 15.5.18(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) react: 19.2.7 '@chakra-ui/react@2.10.10(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(framer-motion@11.18.2(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': @@ -5285,6 +5308,10 @@ snapshots: '@oxc-project/types@0.137.0': {} + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@popperjs/core@2.11.8': {} '@protobufjs/aspromise@1.1.2': {} @@ -7262,6 +7289,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -8096,7 +8126,7 @@ snapshots: next-tick@1.1.0: {} - next@15.5.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + next@15.5.18(@playwright/test@1.61.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: '@next/env': 15.5.18 '@swc/helpers': 0.5.15 @@ -8114,6 +8144,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.5.18 '@next/swc-win32-arm64-msvc': 15.5.18 '@next/swc-win32-x64-msvc': 15.5.18 + '@playwright/test': 1.61.1 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -8328,6 +8359,14 @@ snapshots: nan: 2.27.0 optional: true + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss@8.4.31: diff --git a/src/context/index.tsx b/src/context/index.tsx index 20bd752..a4869d9 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -9,7 +9,13 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { type ReactNode, useState } from "react"; function ContextProvider({ children }: { children: ReactNode }) { - const [queryClient] = useState(() => new QueryClient()); + const [queryClient] = useState(() => { + const qc = new QueryClient(); + if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_E2E === 'true') { + (window as any).__queryClient = qc; + } + return qc; + }); return (