Skip to content

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:

ServiceCommandPort/URL
RAG Workerwrangler devhttp://localhost:8787
CSR Cockpitnpm run devhttp://localhost:6410
WBP Portalnpm run devhttp://localhost:4424
Admin Dashboardnpm run devhttp://localhost:4426
Docs sitepnpm devhttp://localhost:4321
Main websitenpm run devVaries

Start the RAG Worker locally

Terminal window
cd teei-knowledge/rag-worker
# Start with local dev environment
wrangler dev --env dev

The 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:

Terminal window
# 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:

Terminal window
curl http://localhost:8787/health

Expected 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

Terminal window
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:

  1. Vectorize has content ingested (wrangler vectorize list teei-knowledge-index)
  2. 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

Terminal window
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

Terminal window
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:

Terminal window
wrangler d1 execute teei-knowledge-db \
--command "SELECT source_title, role, platform FROM chunks LIMIT 20"

Playwright tests

Prerequisites

Terminal window
# In the platform project (e.g., csr-cockpit)
pnpm add -D @playwright/test
npx playwright install chromium

Basic widget visibility test

tests/chat-widget.spec.ts
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

Terminal window
# 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.ts

Checking 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:

  1. Go to AI > AI Gateway
  2. Select the teei-knowledge gateway
  3. 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".