Testing the Integration
Testing the Integration
This guide covers how to run the full stack locally, verify the widget works correctly, and write Playwright tests for widget interactions.
Local stack setup
The widget requires the RAG Worker running locally. The stack:
| Service | Command | Port/URL |
|---|---|---|
| RAG Worker | wrangler dev | http://localhost:8787 |
| CSR Cockpit | npm run dev | http://localhost:6410 |
| WBP Portal | npm run dev | http://localhost:4424 |
| Admin Dashboard | npm run dev | http://localhost:4426 |
| Docs site | pnpm dev | http://localhost:4321 |
| Main website | npm run dev | Varies |
Start the RAG Worker locally
cd teei-knowledge/rag-worker
# Start with local dev environmentwrangler dev --env devThe Worker starts at http://localhost:8787. Local development uses the [env.dev] configuration from wrangler.toml, which sets ALLOWED_ORIGINS = "http://localhost:*".
Configure the widget to use localhost API
In any platform’s layout file, use an environment variable to switch the API URL:
---// In CockpitLayout.astro (or any other layout)const apiUrl = import.meta.env.DEV ? 'http://localhost:8787' : 'https://knowledge-api.theeducationalequalityinstitute.org';---
<ChatProvider client:idle apiUrl={apiUrl} platform="csr-cockpit" userRole={knowledgeRole}> <ChatWidget /></ChatProvider>Or set it as an environment variable in .env:
# In .env (never commit this file)PUBLIC_KNOWLEDGE_API_URL=http://localhost:8787---const apiUrl = import.meta.env.PUBLIC_KNOWLEDGE_API_URL ?? 'https://knowledge-api.theeducationalequalityinstitute.org';---Manual verification: health check
Before testing the widget, verify the Worker is running and Vectorize is connected:
curl http://localhost:8787/healthExpected response:
{ "status": "ok", "vectorize": true, "d1": true, "environment": "development"}If vectorize: false — Vectorize is not reachable. This means no vectors have been ingested or the Vectorize binding is misconfigured. Run the ingestion scripts first.
If d1: false — D1 is not connected. Check wrangler.toml for the correct database_id.
Manual verification: sending a test query
curl -X POST http://localhost:8787/api/chat \ -H "Content-Type: application/json" \ -d '{ "query": "What is Mentors for Ukraine?", "platform": "website", "user_role": "public", "language": "en" }'You should receive a streaming response (Server-Sent Events or text/event-stream). The first chunk will appear immediately if embeddings exist in Vectorize.
If the response is empty or returns an error, check:
- Vectorize has content ingested (
wrangler vectorize list teei-knowledge-index) - D1 has the corresponding chunks (
wrangler d1 execute teei-knowledge-db --command "SELECT COUNT(*) FROM chunks")
Verifying role-based filtering
The widget filters content by user_role. To verify filtering works:
Test as public
curl -X POST http://localhost:8787/api/chat \ -H "Content-Type: application/json" \ -d '{ "query": "Show me the admin onboarding guide", "platform": "website", "user_role": "public", "language": "en" }'Expected: The response either returns no results or returns only publicly-tagged content. Admin-specific procedures should not appear.
Test as admin
curl -X POST http://localhost:8787/api/chat \ -H "Content-Type: application/json" \ -d '{ "query": "Show me the admin onboarding guide", "platform": "admin", "user_role": "admin", "language": "en" }'Expected: Full admin-level content returned.
Compare responses
To confirm the filter is working, compare the two responses. If both return identical content, either:
- The content is tagged
role: public(visible to everyone), or - The role filtering is not working correctly
Inspect D1 directly to verify content tags:
wrangler d1 execute teei-knowledge-db \ --command "SELECT source_title, role, platform FROM chunks LIMIT 20"Playwright tests
Prerequisites
# In the platform project (e.g., csr-cockpit)pnpm add -D @playwright/testnpx playwright install chromiumBasic widget visibility test
import { test, expect } from '@playwright/test';
test.describe('TEEI Chat Widget', () => { test('FAB is visible on the dashboard', async ({ page }) => { // Log in first (adjust to your auth flow) await page.goto('http://localhost:6410/demo/');
// Wait for idle hydration await page.waitForLoadState('networkidle');
const fab = page.locator('.teei-chat-fab'); await expect(fab).toBeVisible(); });
test('panel opens when FAB is clicked', async ({ page }) => { await page.goto('http://localhost:6410/demo/'); await page.waitForLoadState('networkidle');
const fab = page.locator('.teei-chat-fab'); await fab.click();
const panel = page.locator('.teei-chat-panel'); await expect(panel).toBeVisible(); });
test('panel closes when close button is clicked', async ({ page }) => { await page.goto('http://localhost:6410/demo/'); await page.waitForLoadState('networkidle');
await page.locator('.teei-chat-fab').click(); await expect(page.locator('.teei-chat-panel')).toBeVisible();
await page.locator('.teei-chat-close').click(); await expect(page.locator('.teei-chat-panel')).not.toBeVisible(); });});Sending a message and checking for response
test('sends a message and receives a response', async ({ page }) => { // Point to local Worker await page.goto('http://localhost:6410/demo/'); await page.waitForLoadState('networkidle');
// Open the widget await page.locator('.teei-chat-fab').click(); await expect(page.locator('.teei-chat-panel')).toBeVisible();
// Type a message in the input const input = page.locator('.teei-chat-input textarea, .teei-chat-input input'); await input.fill('What is the Mentors for Ukraine programme?');
// Send via Enter key await input.press('Enter');
// Wait for a response message to appear const assistantMessage = page.locator('.teei-chat-message--assistant').last(); await expect(assistantMessage).toBeVisible({ timeout: 30_000 });
// Verify some content is present const text = await assistantMessage.textContent(); expect(text).toBeTruthy(); expect(text!.length).toBeGreaterThan(20);});Verifying role-based API request
test('sends correct platform and userRole in API request', async ({ page }) => { const apiRequests: { platform: string; user_role: string }[] = [];
// Intercept POST requests to the chat API page.on('request', (req) => { if (req.url().includes('/api/chat') && req.method() === 'POST') { try { const body = JSON.parse(req.postData() ?? '{}'); apiRequests.push({ platform: body.platform, user_role: body.user_role }); } catch { // ignore parse errors } } });
await page.goto('http://localhost:6410/demo/'); await page.waitForLoadState('networkidle');
await page.locator('.teei-chat-fab').click(); await page.locator('.teei-chat-input textarea, .teei-chat-input input').fill('Hello'); await page.keyboard.press('Enter');
// Wait for the request to fire await page.waitForTimeout(1000);
expect(apiRequests.length).toBeGreaterThan(0); expect(apiRequests[0].platform).toBe('csr-cockpit'); expect(apiRequests[0].user_role).toMatch(/^(viewer|employee|manager|admin)$/);});Widget not visible on excluded pages (main website)
test('widget does not appear on the donate page', async ({ page }) => { await page.goto('http://localhost:4321/donate/'); await page.waitForLoadState('networkidle');
const fab = page.locator('.teei-chat-fab'); await expect(fab).not.toBeVisible();});
test('widget appears on the homepage', async ({ page }) => { await page.goto('http://localhost:4321/'); await page.waitForLoadState('networkidle');
const fab = page.locator('.teei-chat-fab'); await expect(fab).toBeVisible();});Accessibility check on the widget
import AxeBuilder from '@axe-core/playwright';
test('chat panel has no critical accessibility violations', async ({ page }) => { await page.goto('http://localhost:6410/demo/'); await page.waitForLoadState('networkidle');
await page.locator('.teei-chat-fab').click(); await expect(page.locator('.teei-chat-panel')).toBeVisible();
const accessibilityScanResults = await new AxeBuilder({ page }) .include('.teei-chat-widget') .analyze();
const critical = accessibilityScanResults.violations.filter(v => v.impact === 'critical'); expect(critical).toHaveLength(0);});Run all widget tests
# From the platform directory (e.g., csr-cockpit)npx playwright test tests/chat-widget.spec.ts --headed
# Run headless (CI)npx playwright test tests/chat-widget.spec.tsChecking widget loads without errors
In the browser DevTools console, no errors should appear on page load:
// These should not appear:// "Error: useChatWidget must be used inside <ChatProvider>"// "jsxDEV is not a function"// "Cannot read properties of null (reading 'useState')"// "Access to fetch at '...' from origin '...' has been blocked by CORS policy"If you see the useChatWidget must be used inside <ChatProvider> error, <ChatWidget> is being used outside <ChatProvider>. Ensure the widget is wrapped:
<!-- Correct --><ChatProvider ...> <ChatWidget /></ChatProvider>
<!-- Wrong — will throw --><ChatWidget />Verifying AI Gateway caching is active
Cloudflare AI Gateway caches identical queries. In the Cloudflare Dashboard:
- Go to AI > AI Gateway
- Select the
teei-knowledgegateway - Check the Analytics tab for cache hit rate
A healthy cache hit rate (once content is established) should be above 20–30% for a docs/help use case where users ask similar questions. If the hit rate is 0%, check that the gateway name in wrangler.toml matches exactly: AI_GATEWAY_NAME = "teei-knowledge".