mirror of
https://github.com/hanzoai/store.git
synced 2026-10-30 14:54:46 +00:00
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
292 lines
9.9 KiB
TypeScript
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();
|
|
});
|
|
});
|