feat: initial IPFS Portal — Next.js frontend for remote IPFS node management
Multi-file upload with Quick Upload + Crypto Payment tabs. Dashboard with live IPFS metrics, Explorer, Peers, Pins, History, Settings pages. Docker deploy (standalone output), full test suite (16 Playwright tests). Payment integration via IPFSPortalPayment + MockUSDC contracts on zkSync.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('IPFS Portal', () => {
|
||||
const BASE = 'http://localhost:3445';
|
||||
|
||||
test('health endpoint returns 200', async ({ request }) => {
|
||||
const r = await request.get(`${BASE}/api/health`);
|
||||
expect(r.ok()).toBeTruthy();
|
||||
const body = await r.json();
|
||||
expect(body).toHaveProperty('status', 'ok');
|
||||
expect(body).toHaveProperty('kuboApi');
|
||||
});
|
||||
|
||||
test('dashboard loads without errors', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
page.on('console', (msg) => { if (msg.type() === 'error') errors.push(msg.text()); });
|
||||
page.on('pageerror', (err) => errors.push('PAGE: ' + err.message));
|
||||
|
||||
await page.goto(`${BASE}/dashboard`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText('Dashboard');
|
||||
|
||||
const critical = errors.filter(
|
||||
(e) => !e.includes('favicon') && !e.includes('DevTools') && !e.includes('404'),
|
||||
);
|
||||
expect(critical).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('explorer page renders', async ({ page }) => {
|
||||
await page.goto(`${BASE}/explorer`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText(/Explorer/i);
|
||||
});
|
||||
|
||||
test('peers page loads', async ({ page }) => {
|
||||
await page.goto(`${BASE}/peers`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText(/Peers/i);
|
||||
});
|
||||
|
||||
test('pins page loads', async ({ page }) => {
|
||||
await page.goto(`${BASE}/pins`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText(/Pins/i);
|
||||
});
|
||||
|
||||
test('upload page renders with tabs and drop zone', async ({ page }) => {
|
||||
await page.goto(`${BASE}/upload`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText(/Upload/i);
|
||||
// Quick Upload tab should show drop zone with the highlighted "Click to browse" text
|
||||
await expect(page.getByText('Click to browse', { exact: true })).toBeVisible();
|
||||
// Tab switcher should exist
|
||||
await expect(page.getByRole('button', { name: /Quick Upload/i })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Crypto Payment/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test('upload page crypto tab switches correctly', async ({ page }) => {
|
||||
await page.goto(`${BASE}/upload`, { waitUntil: 'networkidle' });
|
||||
// Switch to Crypto tab
|
||||
await page.getByRole('button', { name: 'Crypto Payment' }).click();
|
||||
await expect(page.locator('text=Select a file to see pricing')).toBeVisible();
|
||||
});
|
||||
|
||||
test('IPNS page loads keys', async ({ page }) => {
|
||||
await page.goto(`${BASE}/ipns`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText(/IPNS/i);
|
||||
});
|
||||
|
||||
test('history page loads with empty state', async ({ page }) => {
|
||||
await page.goto(`${BASE}/history`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText(/Upload History/i);
|
||||
await expect(page.locator('text=No upload history yet')).toBeVisible();
|
||||
await expect(page.locator('text=Upload your first file')).toBeVisible();
|
||||
});
|
||||
|
||||
test('settings page loads with editable form', async ({ page }) => {
|
||||
await page.goto(`${BASE}/settings`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText('Settings');
|
||||
// Should show input fields
|
||||
const inputs = page.locator('input');
|
||||
const count = await inputs.count();
|
||||
expect(count).toBeGreaterThanOrEqual(3);
|
||||
// Save and Reset buttons
|
||||
await expect(page.locator('button:has-text("Save Settings")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Reset Defaults")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('IPFS node info via proxy', async ({ request }) => {
|
||||
const r = await request.get(`${BASE}/api/node/info`);
|
||||
expect(r.ok()).toBeTruthy();
|
||||
const body = await r.json();
|
||||
expect(body).toHaveProperty('ID');
|
||||
expect(body).toHaveProperty('AgentVersion');
|
||||
});
|
||||
|
||||
test('swarm peers endpoint', async ({ request }) => {
|
||||
const r = await request.get(`${BASE}/api/peers`);
|
||||
expect(r.ok()).toBeTruthy();
|
||||
const body = await r.json();
|
||||
expect(body).toHaveProperty('Peers');
|
||||
});
|
||||
|
||||
test('repo stats endpoint', async ({ request }) => {
|
||||
const r = await request.get(`${BASE}/api/repo/stats`);
|
||||
expect(r.ok()).toBeTruthy();
|
||||
const body = await r.json();
|
||||
expect(body).toHaveProperty('RepoSize');
|
||||
expect(body).toHaveProperty('NumObjects');
|
||||
});
|
||||
|
||||
test('sidebar navigation links exist and work', async ({ page }) => {
|
||||
await page.goto(`${BASE}/dashboard`, { waitUntil: 'networkidle' });
|
||||
await expect(page.locator('h1')).toContainText('Dashboard');
|
||||
|
||||
const sidebar = page.locator('aside');
|
||||
await expect(sidebar).toBeVisible();
|
||||
const links = sidebar.getByRole('link');
|
||||
const count = await links.count();
|
||||
expect(count).toBeGreaterThanOrEqual(8); // History added
|
||||
|
||||
for (const name of ['Explorer', 'Pins', 'Peers', 'History']) {
|
||||
await sidebar.getByRole('link', { name }).click();
|
||||
await expect(page).toHaveURL(new RegExp(name.toLowerCase()));
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Payment Flow ── */
|
||||
|
||||
test('upload page crypto tab shows payment panel after file selection', async ({ page }) => {
|
||||
await page.goto(`${BASE}/upload`, { waitUntil: 'networkidle' });
|
||||
|
||||
// Switch to Crypto tab
|
||||
await page.getByRole('button', { name: 'Crypto Payment' }).click();
|
||||
|
||||
// Create a dummy file and set it on the crypto file input
|
||||
const fileInput = page.locator('input[type="file"]').last();
|
||||
await fileInput.setInputFiles({
|
||||
name: 'test.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: Buffer.from('hello ipfs payment test'),
|
||||
});
|
||||
|
||||
// Payment panel should now be visible with "Crypto Payment" heading
|
||||
await expect(page.getByRole('heading', { name: 'Crypto Payment' })).toBeVisible();
|
||||
|
||||
// Should either show "not deployed" or "Connect Wallet"
|
||||
const notDeployed = page.locator('text=Payment contract not deployed');
|
||||
const connectWallet = page.locator('button:has-text("Connect Wallet")');
|
||||
|
||||
const isNotDeployed = await notDeployed.isVisible().catch(() => false);
|
||||
if (isNotDeployed) {
|
||||
await expect(notDeployed).toBeVisible();
|
||||
} else {
|
||||
await expect(connectWallet).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('payment verification API accepts address query', async ({ request }) => {
|
||||
// Test with a zero address to verify the API structure works
|
||||
const r = await request.get(`${BASE}/api/payment/verify?address=0x0000000000000000000000000000000000000000`);
|
||||
expect(r.ok()).toBeTruthy();
|
||||
const body = await r.json();
|
||||
|
||||
// Should either show deployed or not
|
||||
expect(body).toHaveProperty('deployed');
|
||||
if (body.deployed) {
|
||||
expect(body).toHaveProperty('stats');
|
||||
expect(body.stats).toHaveProperty('totalBytes');
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user