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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
ci:
name: Lint, Typecheck, Test, Build
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 20

steps:
- name: Checkout
Expand Down Expand Up @@ -49,3 +49,11 @@ jobs:

- name: Test
run: pnpm test

- name: Install Playwright browsers
working-directory: apps/web
run: pnpm exec playwright install --with-deps chromium

- name: E2E tests
working-directory: apps/web
run: pnpm test:e2e
16 changes: 13 additions & 3 deletions CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ report exported. CLI and web inspector.
- ✅ Inspector: failure summary (severity, description, suggested steps)
- ✅ Inspector: Markdown report export (download)

### Inspector Polish (in progress — this PR)
### Inspector Polish (PR #44)

- ✅ Loading state with spinner ("Parsing trace…")
- ✅ Error state improvements (non-sensitive messages, empty input guidance)
Expand All @@ -144,6 +144,14 @@ report exported. CLI and web inspector.
- ✅ Sticky header for better UX on long traces
- ✅ Analyze button shows "Analyzing…" and disables during parsing

### Playwright Smoke Tests (in progress — this PR)

- ✅ `playwright.config.ts` — chromium, auto-start dev server
- ✅ Landing page tests: page loads, hero, CTA links, features, footer
- ✅ Navigation tests: landing → inspector, landing → docs
- ✅ Inspector tests: empty state, sample scenario → timeline, failures, event click → message inspector, export button, invalid input error
- ✅ CI workflow updated: install browsers + run E2E after unit tests

## What's Next

1. **Issue #20** → complete (PR #33): data model + parser + normalizer
Expand All @@ -155,8 +163,10 @@ report exported. CLI and web inspector.
7. **Issue #26** → complete (PR #42): Next.js app scaffold (Next.js + Tailwind, routes for /, /inspector, /docs)
8. **Issue #27** → complete (PR #43): Landing page (hero, features, architecture, quick start, footer)
9. **Issue #28** → complete (PR #43): Inspector (trace input + timeline + message inspector + failures + report export)
10. **Issue #29** (this PR) → complete: Inspector polish (loading states, responsive, keyboard nav)
11. **Issue #30**: Playwright smoke tests
10. **Issue #29** → complete (PR #44): Inspector polish (loading states, responsive, keyboard nav)
11. **Issue #30** (this PR) → complete: Playwright smoke tests
12. **Issue #31**: Docs content (quickstart, glossary, architecture, CLI reference, API reference)
13. **Issue #32**: Release v0.1.0

## Known Blockers / Decisions Pending

Expand Down
6 changes: 6 additions & 0 deletions apps/web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
6 changes: 4 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
"start": "next start",
"lint": "eslint",
"typecheck": "tsc --noEmit",
"clean": "rm -rf .next .turbo"
"clean": "rm -rf .next .turbo",
"test:e2e": "playwright test"
},
"dependencies": {
"@ocpp-debugkit/core": "workspace:*",
"@ocpp-debugkit/scenarios": "workspace:*",
"@ocpp-debugkit/reporter": "workspace:*",
"@ocpp-debugkit/scenarios": "workspace:*",
"next": "16.2.10",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@playwright/test": "^1.61.1",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
Expand Down
26 changes: 26 additions & 0 deletions apps/web/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: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
90 changes: 90 additions & 0 deletions apps/web/tests/inspector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { test, expect } from '@playwright/test';

test.describe('Inspector', () => {
test('page loads with empty state', async ({ page }) => {
await page.goto('/inspector');
await expect(page.getByText('OCPP Inspector')).toBeVisible();
await expect(page.getByText('Paste OCPP Trace')).toBeVisible();
await expect(page.getByText('Sample Scenarios')).toBeVisible();
// Empty state message
await expect(
page.getByText('Paste a trace, upload a file, or select a sample scenario'),
).toBeVisible();
});

test('load sample scenario and see timeline', async ({ page }) => {
await page.goto('/inspector');

// Click the normal-session scenario button
await page.getByRole('button', { name: 'normal-session' }).click();

// The textarea should now have content
const textarea = page.locator('textarea');
await expect(textarea).not.toBeEmpty();

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

// Wait for results — stat cards should appear
await expect(page.getByText('Events').first()).toBeVisible({ timeout: 10000 });
await expect(page.getByText('Sessions').first()).toBeVisible();

// Timeline should be present
await expect(page.getByText('Event Timeline')).toBeVisible();
});

test('load failed-auth scenario and see failures', async ({ page }) => {
await page.goto('/inspector');

await page.getByRole('button', { name: 'failed-auth' }).click();
await page.getByRole('button', { name: 'Analyze' }).click();

// Wait for failure to appear
await expect(page.getByText('FAILED_AUTHORIZATION')).toBeVisible({
timeout: 10000,
});
});

test('click event in timeline to inspect message', async ({ page }) => {
await page.goto('/inspector');

await page.getByRole('button', { name: 'normal-session' }).click();
await page.getByRole('button', { name: 'Analyze' }).click();

// Wait for timeline
await expect(page.getByText('Event Timeline')).toBeVisible({ timeout: 10000 });

// Click first event button in the timeline
const firstEvent = page.locator('[data-event-id]').first();
await firstEvent.click();

// Message inspector should show details
await expect(page.getByText('Message Inspector')).toBeVisible();
await expect(page.getByText('Raw Message')).toBeVisible();
await expect(page.getByText('Payload')).toBeVisible();
});

test('export report button appears after analysis', async ({ page }) => {
await page.goto('/inspector');

await page.getByRole('button', { name: 'normal-session' }).click();
await page.getByRole('button', { name: 'Analyze' }).click();

// Wait for results
await expect(page.getByText('Event Timeline')).toBeVisible({ timeout: 10000 });

// Export button should be visible
await expect(page.getByRole('button', { name: 'Export Report' })).toBeVisible();
});

test('handles invalid input gracefully', async ({ page }) => {
await page.goto('/inspector');

// Type invalid JSON
await page.locator('textarea').fill('{ invalid json }');
await page.getByRole('button', { name: 'Analyze' }).click();

// Should show error message (not crash)
await expect(page.getByText('Error:')).toBeVisible({ timeout: 10000 });
});
});
62 changes: 62 additions & 0 deletions apps/web/tests/landing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { test, expect } from '@playwright/test';

test.describe('Landing page', () => {
test('page loads and shows hero', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle('OCPP DebugKit');
await expect(page.locator('h1')).toHaveText('OCPP DebugKit');
});

test('hero has CTA links', async ({ page }) => {
await page.goto('/');
const inspectorLink = page.getByRole('link', { name: 'Try Inspector' });
await expect(inspectorLink).toBeVisible();
await expect(inspectorLink).toHaveAttribute('href', '/inspector');

const githubLink = page.getByRole('link', { name: 'View on GitHub' });
await expect(githubLink).toBeVisible();
await expect(githubLink).toHaveAttribute('href', /github\.com/);
});

test('features section is present', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Features' })).toBeVisible();
await expect(page.getByText('Inspect').first()).toBeVisible();
await expect(page.getByText('Detect').first()).toBeVisible();
await expect(page.getByText('Test').first()).toBeVisible();
await expect(page.getByText('Report').first()).toBeVisible();
});

test("what it's not section is present", async ({ page }) => {
await page.goto('/');
await expect(page.getByText("What it's not")).toBeVisible();
await expect(page.getByText('Not a CSMS')).toBeVisible();
await expect(page.getByText('Not a simulator')).toBeVisible();
await expect(page.getByText('Not a compliance tool')).toBeVisible();
});

test('footer has links', async ({ page }) => {
await page.goto('/');
const footer = page.locator('footer');
await expect(footer.getByRole('link', { name: 'GitHub' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'npm' })).toBeVisible();
await expect(footer.getByRole('link', { name: 'Docs' })).toBeVisible();
await expect(footer.getByText('Apache 2.0 License')).toBeVisible();
});
});

test.describe('Navigation', () => {
test('can navigate from landing to inspector', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'Try Inspector' }).click();
await expect(page).toHaveURL('/inspector');
await expect(page.getByText('OCPP Inspector')).toBeVisible();
});

test('can navigate to docs', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'Docs' }).first().click();
await expect(page).toHaveURL('/docs');
await expect(page.getByRole('heading', { name: 'Documentation' })).toBeVisible();
});
});
Loading
Loading