Files
store/e2e/store.spec.ts
zeekay c4c9a0aa95 Fix all remaining issues found by CTO agent swarm
High Priority Fixes:
- ✅ Add View Details button for app navigation to detail pages
- ✅ Add footer links for Guidelines, Terms, and Privacy pages
- ✅ Rename data files to match store.json IDs (hanzo→shinkai prefix)
- ✅ Fix WalletConnect projectId - make optional to avoid console errors
- ✅ Add favicon using Hanzo logo

Medium Priority Fixes:
- ✅ Remove console statements from production code
- ✅ Remove unused Github import from page-client
- ✅ Add URL sanitization for security (XSS prevention)
- ✅ Create URL validation utility function

Code Quality Improvements:
- ✅ Add TypeScript type definitions for @hanzo/ui
- ✅ Add explicit types for event handlers
- ✅ Add .env.example for WalletConnect setup
- ✅ Add LLM.md with comprehensive documentation
- ✅ Fix implicit any types in test files

Security Enhancements:
- Sanitize all external URLs before rendering
- Validate URL protocols (http/https only)
- Prevent XSS through URL injection

Files Changed:
- app/page.tsx - navigation, footer links, URL validation
- app/apps/[id]/page-client.tsx - URL validation, remove console
- app/apps/[id]/page.tsx - remove console
- lib/wagmi.ts - make WalletConnect optional
- lib/url-utils.ts - NEW: URL validation utility
- types/hanzo-ui.d.ts - NEW: TypeScript declarations
- .env.example - NEW: environment variable template
- .github/workflows/deploy.yml - add WalletConnect env var
- public/favicon.ico - NEW: Hanzo logo favicon
- data files - rename to match IDs
2025-11-06 02:04:48 -08:00

292 lines
9.9 KiB
TypeScript

import { test, expect } from '@playwright/test';
test.describe('Hanzo Store', () => {
// Helper to wait for page to be fully loaded
async function waitForPageLoad(page: any) {
// Wait for the main heading to appear (this means React has hydrated)
await page.waitForSelector('h1:has-text("Hanzo Store")', { timeout: 30000 });
// Wait for store data to finish loading (either apps appear or "Loading..." disappears)
await page.waitForSelector('[class*="grid"]', { timeout: 30000 }).catch(() => {
// Grid might not appear if there are no apps
});
}
test('should load the store page successfully', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
// Check page title
await expect(page).toHaveTitle(/Hanzo Store/);
// Check if main header is visible
await expect(page.getByRole('heading', { name: 'Hanzo Store' })).toBeVisible();
});
test('should display Hanzo logo', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
// Check if HanzoLogo component is rendered (it renders as SVG)
const logo = page.locator('svg').first();
await expect(logo).toBeVisible();
});
test('should display Connect Wallet button', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
// Check if Connect button from RainbowKit is visible
const connectButton = page.getByText('Connect Wallet').or(page.locator('[data-testid="rk-connect-button"]'));
await expect(connectButton.first()).toBeVisible();
});
test('should display app count and category count', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
// Check if stats are displayed
await expect(page.locator('text=/\\d+ Apps/')).toBeVisible();
await expect(page.locator('text=/\\d+ Categories/')).toBeVisible();
});
test('should display search input', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
// Check if search input is visible
const searchInput = page.getByPlaceholder('Search apps...');
await expect(searchInput).toBeVisible();
});
test('should filter apps by search query', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
// Get initial app count
const initialCount = await page.locator('text=/\\d+ apps? found/').textContent();
// Type in search box
await page.getByPlaceholder('Search apps...').fill('test');
// Wait a bit for filtering
await page.waitForTimeout(300);
// Check that results changed
const filteredCount = await page.locator('text=/\\d+ apps? found/').textContent();
// Results should be different (unless there are no test apps)
// We just verify the filter UI is working
expect(filteredCount).toBeTruthy();
});
test('should display type filters (All Types, Agent, Tool)', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Check if type filter buttons exist
await expect(page.getByRole('button', { name: /All Types/ })).toBeVisible();
await expect(page.getByRole('button', { name: /Agent/ })).toBeVisible();
await expect(page.getByRole('button', { name: /Tool/ })).toBeVisible();
});
test('should filter apps by type', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Click on Tool type filter
await page.getByRole('button', { name: /Tool/ }).first().click();
// Wait for filtering
await page.waitForTimeout(300);
// Verify the button is now selected (has different styling)
const toolButton = page.getByRole('button', { name: /Tool/ }).first();
await expect(toolButton).toBeVisible();
});
test('should display category filters', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Check if "All" category button exists
await expect(page.getByRole('button', { name: /^All/ })).toBeVisible();
});
test('should filter apps by category', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Get all category buttons
const categoryButtons = page.getByRole('button').filter({ hasText: /^\w+\s+\(\d+\)$/ });
const count = await categoryButtons.count();
// If there are category buttons beyond "All", click one
if (count > 1) {
await categoryButtons.nth(1).click();
await page.waitForTimeout(300);
// Verify results updated
await expect(page.locator('text=/\\d+ apps? found/')).toBeVisible();
}
});
test('should display app cards', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Wait for apps to load
await page.waitForSelector('[class*="grid"]', { timeout: 10000 });
// Check if at least one app card is visible
const appCards = page.locator('[class*="rounded-xl"][class*="border"]').filter({ hasText: /Install in Hanzo/ });
await expect(appCards.first()).toBeVisible();
});
test('should display app details in card', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Wait for first app card
const firstCard = page.locator('[class*="rounded-xl"][class*="border"]').first();
await firstCard.waitFor({ state: 'visible', timeout: 10000 });
// Check if app card contains essential elements
// Name should be visible
await expect(firstCard.locator('h3, [class*="text-xl"]').first()).toBeVisible();
// Install button should be visible
await expect(firstCard.getByText(/Install in Hanzo/)).toBeVisible();
});
test('should show copy install command button', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Wait for first app card
const firstCard = page.locator('[class*="rounded-xl"][class*="border"]').first();
await firstCard.waitFor({ state: 'visible', timeout: 10000 });
// Check if copy button exists (might not be visible for all apps)
const copyButtons = page.locator('button').filter({ hasText: /npm|npx|yarn/ });
const count = await copyButtons.count();
// We just verify the UI can render copy buttons if they exist
expect(count).toBeGreaterThanOrEqual(0);
});
test('should show GitHub repository link', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Check if any "View on GitHub" links exist
const githubLinks = page.getByText('View on GitHub');
const count = await githubLinks.count();
// At least some apps should have GitHub links
expect(count).toBeGreaterThanOrEqual(0);
});
test('should display featured badge on featured apps', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Check if any featured badges exist
const featuredBadges = page.getByText(/⭐\s*Featured/);
const count = await featuredBadges.count();
// Featured badges might not always be present
expect(count).toBeGreaterThanOrEqual(0);
});
test('should display footer with contribution instructions', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
// Scroll to footer
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
// Check if footer text is visible
await expect(page.getByText(/Want to add your MCP server/)).toBeVisible();
await expect(page.getByText(/data\/agents\//).or(page.getByText(/data\/tools\//))).toBeVisible();
});
test('should handle empty search results gracefully', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Search for something that definitely doesn't exist
await page.getByPlaceholder('Search apps...').fill('xyzabc123nonexistent');
await page.waitForTimeout(300);
// Should show "0 apps found" or "No apps found"
const noResultsText = page.locator('text=/0 apps? found|No apps found/');
await expect(noResultsText.first()).toBeVisible();
});
test('should use proper Hanzo brand colors', async ({ page }) => {
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Check if the page has dark mode applied
const html = page.locator('html');
const classes = await html.getAttribute('class');
// Should have dark class or proper theming
expect(classes).toBeTruthy();
// Check if RainbowKit has the custom Hanzo purple accent color (#7c3aed)
const rkStyles = page.locator('[data-rk] style');
const styleContent = await rkStyles.first().textContent();
// Should contain our custom Hanzo purple
expect(styleContent).toContain('#7c3aed');
});
test('should be responsive on mobile viewport', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
await waitForPageLoad(page);
await page.waitForLoadState('networkidle');
// Check if content is still visible and accessible
await expect(page.getByRole('heading', { name: 'Hanzo Store' })).toBeVisible();
await expect(page.getByPlaceholder('Search apps...')).toBeVisible();
});
test('should load store.json data successfully', async ({ page }) => {
// Navigate and wait for the store data to be fetched
await page.goto('/');
await waitForPageLoad(page);
// Wait for the loading state to disappear
await page.waitForSelector('text=Loading store...', { state: 'hidden', timeout: 10000 });
// Verify that apps are displayed (meaning data loaded successfully)
await expect(page.locator('text=/\\d+ apps? found/')).toBeVisible();
});
});