Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions e2e/onboarding-tour.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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",
Expand Down
26 changes: 26 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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,
},
});
55 changes: 51 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 36 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,56 @@
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 (
<footer className="border-t border-outline-variant/30 py-4 text-center">
<button
onClick={() => startTour(true)}
data-testid="restart-tour"
className="font-heading text-[10px] uppercase tracking-widest text-outline transition-colors hover:text-on-surface-variant"
>
Take the tour
</button>
</footer>
);
}

export function App() {
return (
<div className="flex min-h-screen flex-col">
<Header />
<AutoSign />
<TourAutoStart />
<main className="mx-auto w-full max-w-[720px] flex-1 px-4 pb-16 pt-8 sm:px-6 sm:pb-24 sm:pt-10">
<Routes>
<Route path="/send" element={<Send />} />
<Route path="/receive" element={<Receive />} />
<Route path="*" element={<Navigate to="/send" replace />} />
</Routes>
</main>
<Footer />
</div>
);
}
3 changes: 3 additions & 0 deletions src/components/StellarSend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ export function StellarSend() {
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="st:xlm:..."
data-tour="recipient-input"
className="h-12 w-full border border-outline-variant bg-surface px-4 pr-20 font-mono text-sm text-primary placeholder:text-outline focus:border-primary"
/>
<button
Expand All @@ -240,6 +241,7 @@ export function StellarSend() {
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="0.0"
data-tour="amount-input"
className="h-12 w-full border border-outline-variant bg-surface px-4 pr-16 font-heading text-2xl text-primary placeholder:text-outline focus:border-primary"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 font-mono text-xs text-outline">
Expand Down Expand Up @@ -268,6 +270,7 @@ export function StellarSend() {
<button
onClick={handleSend}
disabled={!recipient || !amount || isPending}
data-tour="send-button"
className="h-12 w-full bg-primary font-heading text-[13px] font-semibold uppercase tracking-widest text-surface transition-colors hover:brightness-110 disabled:opacity-30"
>
{isPending ? 'Confirm in wallet...' : 'Send Privately'}
Expand Down
4 changes: 2 additions & 2 deletions src/components/WalletConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ function FreighterButton() {

if (isConnected && address) {
return (
<button onClick={disconnect} className={btnConnected}>
<button onClick={disconnect} className={btnConnected} data-tour="wallet-connect">
{address.slice(0, 4)}...{address.slice(-4)}
</button>
);
}

return (
<button onClick={connect} className={btnBase}>
<button onClick={connect} className={btnBase} data-tour="wallet-connect">
Connect Freighter
</button>
);
Expand Down
Loading