diff --git a/.changeset/dry-cobras-rescue.md b/.changeset/dry-cobras-rescue.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/dry-cobras-rescue.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.github/workflows/pr-tests-e2e-assets.yml b/.github/workflows/pr-tests-e2e-assets.yml new file mode 100644 index 000000000..b8a5ed94f --- /dev/null +++ b/.github/workflows/pr-tests-e2e-assets.yml @@ -0,0 +1,38 @@ +name: Tests E2E - Assets Testnet + +on: + pull_request: + branches: [main, master, sdk-v2] + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + tests-e2e-assets: + name: Test + timeout-minutes: 10 + runs-on: buildjet-8vcpu-ubuntu-2204 + steps: + - uses: actions/checkout@v3 + - uses: FuelLabs/github-actions/setups/node@master + with: + node-version: 20.11.0 + pnpm-version: 9.5.0 + + - uses: ./.github/actions/setup-playwright + + - name: Run E2E Tests - Assets Testnet + run: xvfb-run --auto-servernum -- pnpm test:e2e:assets + timeout-minutes: 15 + env: + NODE_ENV: test + READONLY_TESTNET_ASSETS_VIEW: ${{secrets.READONLY_TESTNET_ASSETS_VIEW}} + + - name: Upload Test Report + if: failure() + uses: actions/upload-artifact@v3 + with: + name: e2e-test-report + path: packages/e2e-assets/playwright-html \ No newline at end of file diff --git a/.github/workflows/pr-tests-e2e.yml b/.github/workflows/pr-tests-e2e.yml index 0a601c205..bf5860c45 100644 --- a/.github/workflows/pr-tests-e2e.yml +++ b/.github/workflows/pr-tests-e2e.yml @@ -87,6 +87,7 @@ jobs: timeout-minutes: 15 env: NODE_ENV: test + READONLY_TESTNET_ASSETS_VIEW: ${{ secrets.READONLY_TESTNET_ASSETS_VIEW }} - uses: actions/upload-artifact@v4 if: always() diff --git a/package.json b/package.json index 78c41c891..e88a941ca 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "test:e2e": "NODE_ENV=test pnpm build:crx && playwright test --config=packages/app/playwright.config.ts --project=chromium", "test:e2e:beta": "NODE_ENV=test pnpm build:crx && playwright test --config=packages/app/playwright.config.ts --project=chrome-beta", "test:e2e:crx-lock": "NODE_ENV=test pnpm build:crx && playwright test --config=packages/app/playwright.crx-lock.config.ts", + "test:e2e:assets": "NODE_ENV=test pnpm playwright test --config=packages/e2e-assets/playwright.config.ts", "test:e2e:contracts": "NODE_ENV=test pnpm build:crx && pnpm --filter=@fuel-wallet/e2e-contract-tests test:e2e", "test:appfile": "pnpm --filter=fuels-wallet test --", "ts:check": "turbo run ts:check", diff --git a/packages/app/.env.example b/packages/app/.env.example index fa50db5b1..69b452d2c 100644 --- a/packages/app/.env.example +++ b/packages/app/.env.example @@ -4,7 +4,6 @@ VITE_FUEL_PROVIDER_URL=http://localhost:4000/v1/graphql VITE_FUEL_FAUCET_URL=http://localhost:4040 VITE_EXPLORER_URL=https://app.fuel.network/ VITE_MNEMONIC_WORDS=12 -VITE_ADDR_OWNER=0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298 GENESIS_SECRET=0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298 VITE_AUTO_LOCK_IN_MINUTES=1 VITE_SENTRY_DSN= diff --git a/packages/app/env.d.ts b/packages/app/env.d.ts index b72c69438..29cf4d118 100644 --- a/packages/app/env.d.ts +++ b/packages/app/env.d.ts @@ -5,7 +5,6 @@ declare namespace NodeJS { readonly VITE_FUEL_PROVIDER_URL: string; readonly VITE_FUEL_FAUCET_URL: string; readonly VITE_MNEMONIC_WORDS: string; - readonly VITE_ADDR_OWNER: string; readonly VITE_CRX_NAME: string; readonly VITE_CRX_VERSION_API: string; readonly VITE_CRX_RELEASE: string; diff --git a/packages/app/src/config.ts b/packages/app/src/config.ts index ae1361212..923356871 100644 --- a/packages/app/src/config.ts +++ b/packages/app/src/config.ts @@ -4,7 +4,6 @@ export const { VITE_MNEMONIC_WORDS, VITE_FUEL_PROVIDER_URL, VITE_FUEL_FAUCET_URL, - VITE_ADDR_OWNER, VITE_APP_VERSION, VITE_DATABASE_VERSION, VITE_CRX_NAME, diff --git a/packages/app/src/vite-env.d.ts b/packages/app/src/vite-env.d.ts index 979264c44..375f20f0d 100644 --- a/packages/app/src/vite-env.d.ts +++ b/packages/app/src/vite-env.d.ts @@ -7,7 +7,6 @@ interface ImportMetaEnv { readonly VITE_APP_VERSION: string; readonly VITE_CRX: string; readonly VITE_CRX_VERSION_API: string; - readonly VITE_ADDR_OWNER: string; readonly VITE_CRX_NAME: string; readonly VITE_AUTO_LOCK_IN_MINUTES: number; readonly VITE_SENTRY_DSN: string; diff --git a/packages/e2e-assets/.env.example b/packages/e2e-assets/.env.example new file mode 100644 index 000000000..67d8cf381 --- /dev/null +++ b/packages/e2e-assets/.env.example @@ -0,0 +1,2 @@ +READONLY_TESTNET_ASSETS_VIEW='this mnemonic is available in the project secrets' +PORT=3000 \ No newline at end of file diff --git a/packages/e2e-assets/.gitignore b/packages/e2e-assets/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/packages/e2e-assets/CHANGELOG.md b/packages/e2e-assets/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/packages/e2e-assets/load.envs.cts b/packages/e2e-assets/load.envs.cts new file mode 100644 index 000000000..08d15bd80 --- /dev/null +++ b/packages/e2e-assets/load.envs.cts @@ -0,0 +1,44 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { config } from 'dotenv'; + +function getVersion() { + const packageJson = JSON.parse( + readFileSync(resolve(__dirname, './package.json')).toString() + ); + return { + version: packageJson.version, + database: packageJson.database, + }; +} + +function getEnvName() { + if (process.env.NODE_ENV === 'production') { + return '.env.production'; + } +} + +// Load from more specific env file to generic -> +// biome-ignore lint/complexity/noForEach: +[getEnvName(), '.env'].forEach((envFile) => { + if (!envFile) return; + config({ + path: resolve(__dirname, envFile), + }); +}); + +export function getPublicEnvs() { + const WHITELIST = ['NODE_ENV', 'PUBLIC_URL']; + return Object.fromEntries( + Object.entries(process.env).filter(([key]) => + WHITELIST.some((k) => k === key || key.match(/^VITE_/)) + ) + ); +} + +// Export the version to be used on database +// and application level +const versions = getVersion(); +process.env.PORT = '3000'; +process.env.VITE_APP_VERSION = process.env.VITE_APP_VERSION || versions.version; +process.env.VITE_DATABASE_VERSION = versions.database; diff --git a/packages/e2e-assets/package.json b/packages/e2e-assets/package.json new file mode 100644 index 000000000..543a1f1a9 --- /dev/null +++ b/packages/e2e-assets/package.json @@ -0,0 +1,9 @@ +{ + "name": "@fuel-wallet/e2e-assets", + "private": true, + "version": "0.1.0", + "type": "module", + "devDependencies": { + "@playwright/test": "1.46.1" + } +} diff --git a/packages/e2e-assets/playwright.config.ts b/packages/e2e-assets/playwright.config.ts new file mode 100644 index 000000000..377530072 --- /dev/null +++ b/packages/e2e-assets/playwright.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from '@playwright/test'; +import './load.envs.cts'; +const PORT = process.env.PORT || 3000; +const IS_CI = process.env.CI; + +export default defineConfig({ + workers: 2, + retries: IS_CI ? 1 : 0, + testMatch: 'playwright/**/*.test.ts', + testDir: 'playwright/', + outputDir: 'playwright-results/', + maxFailures: IS_CI ? 2 : undefined, + reporter: [ + ['list', { printSteps: true }], + ['html', { outputFolder: './playwright-html/' }], + ], + webServer: { + command: 'NODE_ENV=test pnpm -w run dev:crx', + reuseExistingServer: true, + timeout: 20000, + url: `http://localhost:${PORT}`, + }, + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'on-first-retry', + actionTimeout: 5000, + permissions: ['clipboard-read', 'clipboard-write'], + screenshot: 'only-on-failure', + headless: false, + }, +}); diff --git a/packages/e2e-assets/playwright/commons/index.ts b/packages/e2e-assets/playwright/commons/index.ts new file mode 100644 index 000000000..ab82b019f --- /dev/null +++ b/packages/e2e-assets/playwright/commons/index.ts @@ -0,0 +1,2 @@ +export * from './locator'; +export * from './text'; diff --git a/packages/e2e-assets/playwright/commons/locator.ts b/packages/e2e-assets/playwright/commons/locator.ts new file mode 100644 index 000000000..f4cec0e1f --- /dev/null +++ b/packages/e2e-assets/playwright/commons/locator.ts @@ -0,0 +1,17 @@ +import type { Page } from '@playwright/test'; + +export function getByAriaLabel(page: Page, selector: string) { + return page.locator(`[aria-label="${selector}"]`); +} + +export async function waitAriaLabel(page: Page, selector: string) { + return page.waitForSelector(`[aria-label="${selector}"]`); +} + +export function getInputByName(page: Page, name: string) { + return page.locator(`input[name="${name}"]`); +} + +export function getInputByValue(page: Page, value: string) { + return page.locator(`input[value='${value}']`); +} diff --git a/packages/e2e-assets/playwright/commons/text.ts b/packages/e2e-assets/playwright/commons/text.ts new file mode 100644 index 000000000..a623fbe89 --- /dev/null +++ b/packages/e2e-assets/playwright/commons/text.ts @@ -0,0 +1,29 @@ +import type { Page } from '@playwright/test'; +import { expect } from '@playwright/test'; + +export async function hasText( + page: Page, + text: string | RegExp, + position = 0, + timeout = 5000 +) { + const textFound = page.getByText(text).nth(position); + await expect(textFound).toHaveText(text, { + useInnerText: true, + timeout, + }); + return textFound; +} + +export async function hasNoText( + page: Page, + text: string | RegExp, + position = 0 +) { + return await expect(page.getByText(text).nth(position)).rejects.toThrow(); +} + +export async function hasAriaLabel(page: Page, value: string) { + const selector = await page.waitForSelector(`[aria-label="${value}"]`); + expect(await selector.getAttribute('aria-label')).toBe(value); +} diff --git a/packages/e2e-assets/playwright/crx/assets.test.ts b/packages/e2e-assets/playwright/crx/assets.test.ts new file mode 100644 index 000000000..813bd8d9f --- /dev/null +++ b/packages/e2e-assets/playwright/crx/assets.test.ts @@ -0,0 +1,131 @@ +import { type Browser, type Page, expect, test } from '@playwright/test'; +import { hasText, waitAriaLabel } from '../commons'; + +const loadWallet = async (page: Page, _browser: Browser) => { + await page.goto('http://localhost:3000', { + waitUntil: 'domcontentloaded', + }); + + await test.step('Import wallet', async () => { + await page.getByRole('heading', { name: 'Import seed phrase' }).click(); + await page.getByText('I Agree to the Terms Of Use').click(); + await page.getByRole('button', { name: 'Next: Seed Phrase' }).click(); + const mnemonic = process.env.READONLY_TESTNET_ASSETS_VIEW; + + await page.evaluate(async (text) => { + await navigator.clipboard.writeText(text); + }, mnemonic); + + await page.waitForTimeout(1000); + await page.getByRole('button', { name: 'Paste seed phrase' }).click(); + + await page.getByRole('button', { name: 'Next: Your password' }).click(); + await page.getByPlaceholder('Type your password').fill('qwe123QWE!@#'); + await page.getByPlaceholder('Confirm your password').fill('qwe123QWE!@#'); + await page.getByRole('button', { name: 'Next: Finish set-up' }).click(); + await hasText(page, /Wallet created successfully/i); + await page.goto('http://localhost:3000/#/wallet'); + await waitAriaLabel(page, 'Account 1 selected'); + await page.getByLabel('Selected Network').click(); + await page.getByText('Fuel Sepolia Testnet').click(); + await waitAriaLabel(page, 'Account 1 selected'); + }); +}; + +test.describe('Check assets', () => { + test.describe.configure({ mode: 'parallel' }); + let page: Page; + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + await loadWallet(page, browser); + }); + + test('should show valid asset value 0.002000', async () => { + expect( + await page.getByText('0.002000', { exact: true }).isVisible() + ).toBeTruthy(); + }); + + test('should show USDCIcon AlertTriangle', async () => { + expect( + await page.getByText('USDCIcon AlertTriangle').isVisible() + ).toBeTruthy(); + }); + + test('should show 1 SCAM NFT', async () => { + expect(await page.getByText('1 SCAM').isVisible()).toBeTruthy(); + }); + + // Verified assets should never show the (Add) button + test('should not show (Add) button for verified assets', async () => { + expect( + await page.getByRole('button', { name: '(Add)' }).isVisible() + ).toBeFalsy(); + }); + + // Verified assets will never be inside of "Hidden assets" part + test('should not show verified assets in hidden assets', async () => { + // get all h6 text from div.fuel_CardList as an array, and then click Show unknown assets button, and then check if the array added a new element with a name other than Unknown + const h6Texts = await page.$$eval('div.fuel_CardList h6', (els) => + els.map((el) => el.textContent) + ); + await page.getByRole('button', { name: 'Show unknown assets' }).click(); + await page.waitForTimeout(1000); + const h6TextsAfter = await page.$$eval('div.fuel_CardList h6', (els) => + els.map((el) => el.textContent) + ); + expect(h6TextsAfter.length).toBeGreaterThan(h6Texts.length); + + for (const el of h6Texts) { + expect(el.includes('(Add)')).toBeFalsy(); + } + // removing all elements from h6TextsAfter that are in h6Texts + const diff = h6TextsAfter.filter((el) => !h6Texts.includes(el)); + // all elements in diff should include Unknown + for (const el of diff) { + expect(el.includes('Unknown')).toBeTruthy(); + } + }); +}); + +test.describe('Check assets', () => { + let page: Page; + test.beforeAll(async ({ browser }) => { + page = await browser.newPage(); + await loadWallet(page, browser); + }); + + test('Should add unknown asset', async () => { + await page.getByRole('button', { name: 'Show unknown assets' }).click(); + await page.getByRole('button', { name: '(Add)' }).nth(1).click(); + await page.getByPlaceholder('Asset name').fill('Token 2'); + await page.getByPlaceholder('Asset symbol').fill('TKN2'); + await page.getByLabel('Save Asset').click(); + await page.waitForTimeout(1000); + await page.reload({ waitUntil: 'domcontentloaded' }); + await page.waitForTimeout(1000); + await waitAriaLabel(page, 'Account 1 selected'); + await page.waitForTimeout(1000); + expect(await page.getByText('1 TKN2').isVisible()).toBeTruthy(); + + // The following tests are disabled because the added tokens need a refresh to show up. Fix FE-1122 and enable these. + + // await page.waitForTimeout(1000); + // await page.reload({ waitUntil: 'domcontentloaded' }); + // await page.waitForTimeout(1000); + // // Non-verified asset that was added to asset list will never be inside of "Hidden assets" part + // // The TKN2 asset should not be in the hidden assets list + // const h6Texts = await page.$$eval('div.fuel_CardList h6', (els) => + // els.map((el) => el.textContent?.trim()) + // ); + // await page.getByRole('button', { name: 'Show unknown assets' }).click(); + // await page.waitForTimeout(1000); + // const h6TextsAfter = await page.$$eval('div.fuel_CardList h6', (els) => + // els.map((el) => el.textContent?.trim()) + // ); + // const diff = h6TextsAfter.filter((el) => !h6Texts.includes(el)); + // console.log(diff); + // // expect at least one of the elements in diff to be Token 2 + // expect(diff.some((el) => el === 'Token 2')).toBeTruthy(); + }); +}); diff --git a/packages/e2e-assets/tsconfig.node.json b/packages/e2e-assets/tsconfig.node.json new file mode 100644 index 000000000..2ed78c171 --- /dev/null +++ b/packages/e2e-assets/tsconfig.node.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "allowJs": true, + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "target": "es2017", + "outDir": "dist-crx" + }, + "include": [ + "load.envs.cts", + "env.d.ts", + "playwright/**/*.ts", + "./package.json" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f51b32c6e..caf36f662 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -544,7 +544,7 @@ importers: version: 3.0.0 next: specifier: 14.2.12 - version: 14.2.12(@babel/core@7.23.2)(@playwright/test@1.46.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.12(@babel/core@7.23.2)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-mdx-remote: specifier: 4.4.1 version: 4.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -619,6 +619,12 @@ importers: specifier: 2.8.8 version: 2.8.8 + packages/e2e-assets: + devDependencies: + '@playwright/test': + specifier: 1.46.1 + version: 1.46.1 + packages/e2e-contract-tests: dependencies: '@fuels/connectors': @@ -3277,6 +3283,11 @@ packages: engines: {node: '>=18'} hasBin: true + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@pmmmwh/react-refresh-webpack-plugin@0.5.10': resolution: {integrity: sha512-j0Ya0hCFZPd4x40qLzbhGsh9TMtdb+CJQiso+WxLOPNasohq9cc5SNUcwsZaRH6++Xh91Xkm/xHCkuIiIu0LUA==} engines: {node: '>= 10.13'} @@ -10953,11 +10964,21 @@ packages: engines: {node: '>=18'} hasBin: true + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + playwright@1.46.1: resolution: {integrity: sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==} engines: {node: '>=18'} hasBin: true + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + plyr-react@5.3.0: resolution: {integrity: sha512-m36/HrpHwg1N2rq3E31E8/kpAH55vk6qHUg17MG4uu9jbWYxnkN39lLmZQwxW7/qpDPfW5aGUJ6R3u23V0R3zA==} engines: {node: '>=16'} @@ -18927,6 +18948,11 @@ snapshots: dependencies: playwright: 1.46.1 + '@playwright/test@1.49.1': + dependencies: + playwright: 1.49.1 + optional: true + '@pmmmwh/react-refresh-webpack-plugin@0.5.10(react-refresh@0.11.0)(type-fest@2.19.0)(webpack-hot-middleware@2.25.4)(webpack@5.88.2(@swc/core@1.3.92(@swc/helpers@0.5.11))(esbuild@0.18.20))': dependencies: ansi-html-community: 0.0.8 @@ -30238,7 +30264,7 @@ snapshots: transitivePeerDependencies: - supports-color - next@14.2.12(@babel/core@7.23.2)(@playwright/test@1.46.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.12(@babel/core@7.23.2)(@playwright/test@1.49.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.12 '@swc/helpers': 0.5.5 @@ -30259,7 +30285,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.12 '@next/swc-win32-ia32-msvc': 14.2.12 '@next/swc-win32-x64-msvc': 14.2.12 - '@playwright/test': 1.46.1 + '@playwright/test': 1.49.1 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -30717,12 +30743,22 @@ snapshots: playwright-core@1.46.1: {} + playwright-core@1.49.1: + optional: true + playwright@1.46.1: dependencies: playwright-core: 1.46.1 optionalDependencies: fsevents: 2.3.2 + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + optional: true + plyr-react@5.3.0(plyr@3.7.8)(react@18.3.1): dependencies: plyr: 3.7.8