diff --git a/e2e/onboarding-tour.spec.ts b/e2e/onboarding-tour.spec.ts new file mode 100644 index 0000000..2581241 --- /dev/null +++ b/e2e/onboarding-tour.spec.ts @@ -0,0 +1,93 @@ +import { test, expect } from '@playwright/test'; + +const TOUR_KEY = 'wraith.tourCompleted'; + +test.describe('Stellar onboarding tour', () => { + test.beforeEach(async ({ page }) => { + // Start fresh — no tour completion flag + await page.goto('/send'); + await page.evaluate((key) => localStorage.removeItem(key), TOUR_KEY); + }); + + test('auto-starts on first visit and completes happy path', async ({ page }) => { + await page.goto('/send'); + + // Tour popover should appear automatically + const popover = page.locator('.driver-popover'); + await expect(popover).toBeVisible({ timeout: 5000 }); + + // Step 1: wallet connect target + await expect(popover).toContainText('Welcome to Wraith'); + + // Advance through all 5 steps + for (let i = 0; i < 4; i++) { + const nextBtn = page.locator('.driver-popover-next-btn'); + await expect(nextBtn).toBeVisible(); + await nextBtn.click(); + } + + // Step 5: send button — click Done + await expect(popover).toContainText('Send Privately'); + const doneBtn = page.locator('.driver-popover-next-btn'); + await expect(doneBtn).toBeVisible(); + await doneBtn.click(); + + // Popover should be gone + await expect(popover).not.toBeVisible({ timeout: 3000 }); + + // localStorage should be set + const stored = await page.evaluate((key) => localStorage.getItem(key), TOUR_KEY); + expect(stored).toBe('true'); + }); + + test('does not auto-start on subsequent page loads after completion', async ({ page }) => { + // Pre-set the flag + await page.goto('/send'); + await page.evaluate((key) => localStorage.setItem(key, 'true'), TOUR_KEY); + + // Reload + await page.reload(); + await page.waitForTimeout(600); // longer than the 300 ms delay in TourAutoStart + + const popover = page.locator('.driver-popover'); + await expect(popover).not.toBeVisible(); + }); + + test('dismissing tour via close button persists completion flag', async ({ page }) => { + await page.goto('/send'); + + const popover = page.locator('.driver-popover'); + await expect(popover).toBeVisible({ timeout: 5000 }); + + // Click the close/skip button + const closeBtn = page.locator('.driver-popover-close-btn'); + await expect(closeBtn).toBeVisible(); + await closeBtn.click(); + + await expect(popover).not.toBeVisible({ timeout: 3000 }); + + const stored = await page.evaluate((key) => localStorage.getItem(key), TOUR_KEY); + expect(stored).toBe('true'); + }); + + test('"Take the tour" footer button force-restarts the tour', async ({ page }) => { + // Mark tour as completed + await page.goto('/send'); + await page.evaluate((key) => localStorage.setItem(key, 'true'), TOUR_KEY); + await page.reload(); + + // Tour should NOT auto-start + await page.waitForTimeout(600); + const popover = page.locator('.driver-popover'); + await expect(popover).not.toBeVisible(); + + // Click the footer restart button + const restartBtn = page.locator('[data-testid="restart-tour"]'); + await expect(restartBtn).toBeVisible(); + await restartBtn.click(); + + // Tour should now be visible + await expect(popover).toBeVisible({ timeout: 3000 }); + await expect(popover).toContainText('Welcome to Wraith'); + }); +}); diff --git a/package.json b/package.json index 886cfb3..0cd3fc5 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dev": "vite", "build": "tsc --noEmit && vite build", "preview": "vite preview", + "test:e2e": "playwright test", "format": "prettier --write .", "format:check": "prettier --check .", "prepare": "husky" @@ -28,6 +29,7 @@ "@wraith-protocol/sdk": "^1.4.5", "bs58": "^6.0.0", "buffer": "^6.0.3", + "driver.js": "1.3.1", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.6.0", @@ -37,6 +39,7 @@ "devDependencies": { "@commitlint/cli": "^19.0.0", "@commitlint/config-conventional": "^19.0.0", + "@playwright/test": "1.49.1", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.5.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..648ba27 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'list', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'pnpm dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3b5c05..53e82e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: buffer: specifier: ^6.0.3 version: 6.0.3 + driver.js: + specifier: 1.3.1 + version: 1.3.1 react: specifier: ^19.0.0 version: 19.2.5 @@ -78,6 +81,9 @@ importers: '@commitlint/config-conventional': specifier: ^19.0.0 version: 19.8.1 + '@playwright/test': + specifier: 1.49.1 + version: 1.49.1 '@types/react': specifier: ^19.0.0 version: 19.2.14 @@ -732,6 +738,11 @@ packages: resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@rainbow-me/rainbowkit@2.2.10': resolution: {integrity: sha512-8+E4die1A2ovN9t3lWxWnwqTGEdFqThXDQRj+E4eDKuUKyymYD+66Gzm6S9yfg8E95c6hmGlavGUfYPtl1EagA==} engines: {node: '>=12.4'} @@ -2351,6 +2362,9 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} + driver.js@1.3.1: + resolution: {integrity: sha512-MvUdXbqSgEsgS/H9KyWb5Rxy0aE6BhOVT4cssi2x2XjmXea6qQfgdx32XKVLLSqTaIw7q/uxU5Xl3NV7+cN6FQ==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2593,6 +2607,11 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + 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} @@ -3387,6 +3406,16 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -4132,6 +4161,7 @@ packages: uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true valibot@1.3.1: @@ -5277,7 +5307,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.13 - debug: 4.3.4 + debug: 4.4.3 pony-cause: 2.1.11 semver: 7.7.4 uuid: 9.0.1 @@ -5291,7 +5321,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.13 - debug: 4.3.4 + debug: 4.4.3 pony-cause: 2.1.11 semver: 7.7.4 uuid: 9.0.1 @@ -5362,6 +5392,10 @@ snapshots: '@paulmillr/qr@0.2.1': {} + '@playwright/test@1.49.1': + dependencies: + playwright: 1.49.1 + '@rainbow-me/rainbowkit@2.2.10(@tanstack/react-query@5.99.0(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3)(viem@2.47.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(wagmi@2.19.5(@react-native-async-storage/async-storage@1.24.0(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6)))(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.5))(@types/react@19.2.14)(bufferutil@4.1.0)(react-native@0.85.1(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6))(react@19.2.5)(typescript@5.9.3)(utf-8-validate@6.0.6)(viem@2.47.17(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.3.6))(zod@4.3.6))': dependencies: '@tanstack/react-query': 5.99.0(react@19.2.5) @@ -7829,6 +7863,8 @@ snapshots: dependencies: is-obj: 2.0.0 + driver.js@1.3.1: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8112,6 +8148,9 @@ snapshots: fresh@0.5.2: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -8900,8 +8939,8 @@ snapshots: '@adraffy/ens-normalize': 1.11.1 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.9.3)(zod@4.3.6) eventemitter3: 5.0.1 optionalDependencies: @@ -9012,6 +9051,14 @@ snapshots: pirates@4.0.7: {} + playwright-core@1.49.1: {} + + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + pngjs@5.0.0: {} pony-cause@2.1.11: {} diff --git a/src/App.tsx b/src/App.tsx index 41f52e0..b94f7bb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,48 @@ -import { Routes, Route, Navigate } from 'react-router-dom'; +import { useEffect } from 'react'; +import { Routes, Route, Navigate, useLocation } from 'react-router-dom'; import { Header } from '@/components/Header'; import { AutoSign } from '@/components/AutoSign'; +import { useStellarTour } from '@/hooks/useStellarTour'; import Send from '@/pages/Send'; import Receive from '@/pages/Receive'; +function TourAutoStart() { + const location = useLocation(); + const { startTour } = useStellarTour(); + + useEffect(() => { + if (location.pathname === '/send') { + // Small delay to let the DOM settle before driver.js queries elements + const id = setTimeout(() => startTour(), 300); + return () => clearTimeout(id); + } + }, [location.pathname, startTour]); + + return null; +} + +function Footer() { + const { startTour } = useStellarTour(); + + return ( + + ); +} + export function App() { return (