Skip to content
Merged
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
51 changes: 49 additions & 2 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,52 @@ jobs:
- name: Build fetchr binary
run: make build

- name: Run end-to-end tests
- name: Run Go end-to-end tests
run: |
echo "🚀 Starting end-to-end tests..."
echo "🚀 Starting Go end-to-end tests..."
make e2e
env:
GO_TEST_TIMEOUT: 10m

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: dashboard/package-lock.json

- name: Install dashboard dependencies
run: cd dashboard && npm ci

- name: Build dashboard
run: cd dashboard && npm run build

- name: Install Playwright browsers
run: cd dashboard && npx playwright install --with-deps chromium

- name: Start netkit server for Playwright tests
run: |
./bin/netkit serve --port 8080 --admin-port 8081 --dashboard-port 3000 &
echo $! > /tmp/netkit.pid
# Wait for servers to be ready
timeout 30 bash -c 'until curl -s http://localhost:8081/healthz > /dev/null; do sleep 1; done'
timeout 30 bash -c 'until curl -s http://localhost:3000 > /dev/null; do sleep 1; done'
echo "✅ Netkit server is ready"

- name: Run Playwright E2E tests
run: |
echo "🎭 Starting Playwright E2E tests..."
cd dashboard && npm run test:e2e
env:
CI: true

- name: Stop netkit server
if: always()
run: |
if [ -f /tmp/netkit.pid ]; then
kill $(cat /tmp/netkit.pid) || true
fi

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
Expand All @@ -55,6 +94,14 @@ jobs:
*.log
retention-days: 30

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: dashboard/playwright-report/
retention-days: 7

- name: Comment PR on failure
if: failure() && github.event_name == 'push'
uses: actions/github-script@v7
Expand Down
Empty file removed .gitiginore
Empty file.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
coverage.txt
coverage-e2e.txt
dashboard/test-results
dashboard/playwrite-report
netkit

# Embedded dashboard files (generated during build)
internal/dashboard/out/
internal/dashboard/out/

34 changes: 32 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: build test lint clean coverage release version install check ci help all dev commit-feat commit-fix commit-refactor commit-perf commit-security
.PHONY: build-dashboard test-dashboard lint-dashboard clean-dashboard install-dashboard install-backend dashboard-dev serve-dev build-all clean-all
.PHONY: changelog changelog-update changelog-init install-changelog-tools release-patch release-minor release-major release-prerelease release-prerelease-next
.PHONY: commit-docs commit-style commit-test commit-chore commit-breaking check-conventional-commits
.PHONY: commit-docs commit-style commit-test commit-chore commit-breaking check-conventional-commits test-e2e test-e2e-ui test-e2e-headed

# Build variables
BINARY_NAME=netkit
Expand Down Expand Up @@ -36,7 +36,10 @@ help:
@echo "Test commands:"
@echo " test - Run Go unit tests with coverage"
@echo " test-dashboard - Run dashboard tests"
@echo " e2e - Run end-to-end tests"
@echo " e2e - Run Go end-to-end tests"
@echo " test-e2e - Run Playwright E2E tests (headless)"
@echo " test-e2e-ui - Run Playwright E2E tests with UI mode (interactive)"
@echo " test-e2e-headed - Run Playwright E2E tests in headed mode (watch browser)"
@echo ""
@echo "Lint commands:"
@echo " lint - Run golangci-lint for Go code"
Expand Down Expand Up @@ -221,6 +224,33 @@ test-dashboard:
fi
@cd $(DASHBOARD_DIR) && npm run test --legacy-peer-deps

# Run Playwright E2E tests (headless)
test-e2e:
@if [ ! -d "$(DASHBOARD_DIR)" ]; then \
echo "❌ Error: Dashboard directory '$(DASHBOARD_DIR)' not found"; \
exit 1; \
fi
@echo "🎭 Running Playwright E2E tests..."
@cd $(DASHBOARD_DIR) && npm run test:e2e

# Run Playwright E2E tests with UI mode (interactive)
test-e2e-ui:
@if [ ! -d "$(DASHBOARD_DIR)" ]; then \
echo "❌ Error: Dashboard directory '$(DASHBOARD_DIR)' not found"; \
exit 1; \
fi
@echo "🎭 Running Playwright E2E tests in UI mode..."
@cd $(DASHBOARD_DIR) && npm run test:e2e:ui

# Run Playwright E2E tests in headed mode (watch browser)
test-e2e-headed:
@if [ ! -d "$(DASHBOARD_DIR)" ]; then \
echo "❌ Error: Dashboard directory '$(DASHBOARD_DIR)' not found"; \
exit 1; \
fi
@echo "🎭 Running Playwright E2E tests in headed mode..."
@cd $(DASHBOARD_DIR) && npm run test:e2e:headed

# Run end-to-end tests
e2e:
$(GO) test -v -race -coverprofile=coverage-e2e.txt -covermode=atomic ./test/e2e/... -tags=e2e
Expand Down
2 changes: 1 addition & 1 deletion cmd/netkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func main() {
func runServe() {
// Parse command line flags
port := flag.Int("port", 8080, "Port to listen on")
adminPort := flag.Int("admin-port", 0, "Admin port for health checks and metrics (0 to disable)")
adminPort := flag.Int("admin-port", 8081, "Admin port for health checks and metrics (0 to disable)")
historySize := flag.Int("history-size", 1000, "Maximum number of requests to keep in history")
dashboard := flag.Bool("dashboard", true, "Enable web dashboard")
dashboardPort := flag.Int("dashboard-port", 3000, "Dashboard port")
Expand Down
141 changes: 141 additions & 0 deletions dashboard/e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Playwright E2E Tests

This directory contains end-to-end tests for the Netkit dashboard using Playwright.

## Prerequisites

Before running the tests, you need to have:

1. **Netkit backend running** on port 8080 (proxy) and 8081 (admin)
2. **Dashboard dev server running** on port 3000 (or built dashboard)

## Running Tests

### Option 1: Automatic (Recommended for Local Development)

The Playwright config will automatically start the dev server for you:

```bash
# From the dashboard directory
npm run test:e2e
```

**Note:** You still need to start the netkit backend separately:

```bash
# From the root directory, in a separate terminal
make serve-dev
```

### Option 2: Manual Setup

1. Start the netkit backend:
```bash
# From root directory
make serve-dev
```

2. In another terminal, start the dashboard:
```bash
# From root directory
make dashboard-dev
```

3. In a third terminal, run the tests:
```bash
# From dashboard directory
npm run test:e2e
```

### Option 3: Using Make (Full Stack)

From the root directory:

```bash
# Start both backend and dashboard
make dev
```

Then in another terminal:

```bash
# Run E2E tests
make test-e2e
```

## Test Commands

- `npm run test:e2e` - Run tests headless (CI mode)
- `npm run test:e2e:ui` - Run tests with Playwright UI (interactive)
- `npm run test:e2e:headed` - Run tests with visible browser
- `npm run test:e2e:report` - View last test report

Or from root directory with Make:
- `make test-e2e` - Run tests headless
- `make test-e2e-ui` - Run with UI
- `make test-e2e-headed` - Run with visible browser

## CI/CD

In GitHub Actions, the workflow:
1. Builds the netkit backend
2. Builds the dashboard
3. Starts the netkit server on ports 8080/8081/3000
4. Runs the Playwright tests
5. Uploads test reports as artifacts

The tests will run automatically on:
- Push to main branch
- Pull requests to main branch

## Writing Tests

Tests are located in `dashboard/e2e/*.spec.ts`. The tests:

- Use Playwright's page object model
- Target the dashboard at `http://localhost:3000`
- Assume the proxy is running at `http://localhost:8080`
- Assume the admin API is at `http://localhost:8081`

Example test structure:

```typescript
import { test, expect } from '@playwright/test';

test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});

test('should do something', async ({ page }) => {
// Your test code
});
});
```

## Debugging Failed Tests

If tests fail:

1. Check the HTML report: `npm run test:e2e:report`
2. Run with UI mode: `npm run test:e2e:ui`
3. Check screenshots in `test-results/` directory
4. Verify backend is running: `curl http://localhost:8081/healthz`
5. Verify dashboard is running: `curl http://localhost:3000`

## Common Issues

### "Proxy Offline" error
- The dashboard shows "Proxy Offline" when it can't reach the backend
- Ensure `make serve-dev` is running
- Check ports 8080 and 8081 are not in use by other processes

### Tests timeout
- Increase timeout in `playwright.config.ts` if needed
- Check network connectivity
- Verify the backend responds quickly: `time curl http://localhost:8081/healthz`

### Element not found errors
- The dashboard UI may have changed
- Update selectors in test files
- Use Playwright Inspector: `npx playwright test --debug`
92 changes: 92 additions & 0 deletions dashboard/e2e/dashboard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { test, expect } from '@playwright/test';

test.describe('Netkit Dashboard E2E', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});

test('should load the dashboard', async ({ page }) => {
await expect(page).toHaveTitle(/netkit/i);
});

test('should display request builder form', async ({ page }) => {
// Check for method selector (combobox based on actual page structure)
const methodSelector = page.locator('role=combobox').first();
await expect(methodSelector).toBeVisible();

// Check for URL input (textbox with placeholder)
const urlInput = page.getByPlaceholder(/api.example.com/i);
await expect(urlInput).toBeVisible();

// Check for send button
await expect(page.getByRole('button', { name: /send/i })).toBeVisible();
});

test('should make a request through the proxy', async ({ page }) => {
// Fill in the URL
const urlInput = page.getByPlaceholder(/api.example.com/i);
await urlInput.fill('https://httpbin.org/get');

// The method is already GET by default, so we can skip selecting it

// Click send button
await page.getByRole('button', { name: /send/i }).click();

// Wait for response to appear
await page.waitForTimeout(3000);

// Check that the response is displayed (look for 200 status code or success text)
const responseSection = page.locator('text=/200|success/i').first();
await expect(responseSection).toBeVisible({ timeout: 10000 });
});

test('should display proxy request history', async ({ page }) => {
// Look for "Requests History" text which is visible in the sidebar
const historySection = page.locator('text=/Requests History/i').first();
await expect(historySection).toBeVisible();
});

test('should display request builder heading', async ({ page }) => {
// Look for Request Builder heading
const builderHeading = page.locator('text=/Request Builder/i').first();
await expect(builderHeading).toBeVisible();
});

test('should add custom headers to request', async ({ page }) => {
// Find and click add header button
const addHeaderButton = page.getByRole('button', { name: /add header/i });

if (await addHeaderButton.isVisible()) {
await addHeaderButton.click();

// Find the empty header name/value inputs (second row since first has User-Agent)
const headerNameInputs = page.getByPlaceholder(/header name/i);
const headerValueInputs = page.getByPlaceholder(/header value/i);

// Get the last (newly added) header inputs
const newHeaderName = headerNameInputs.last();
const newHeaderValue = headerValueInputs.last();

await newHeaderName.fill('X-Custom-Header');
await newHeaderValue.fill('test-value');

await expect(newHeaderName).toHaveValue('X-Custom-Header');
}
});

test('should handle request errors gracefully', async ({ page }) => {
// Fill in an invalid URL
const urlInput = page.getByPlaceholder(/api.example.com/i);
await urlInput.fill('http://invalid-domain-that-does-not-exist-12345.com');

// Click send
await page.getByRole('button', { name: /send/i }).click();

// Wait for error to appear
await page.waitForTimeout(5000);

// Check that some error indication is shown
const errorIndicator = page.locator('text=/error|failed|could not/i').first();
await expect(errorIndicator).toBeVisible({ timeout: 10000 });
});
});
Loading
Loading