Skip to content

Commit e1fa274

Browse files
committed
ci: run web Jest unit tests and Playwright e2e in CI (parallel to existing jobs)
1 parent 5373d05 commit e1fa274

File tree

6 files changed

+122
-14
lines changed

6 files changed

+122
-14
lines changed

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ jobs:
154154
cd ${{ matrix.package }}
155155
if [ "${{ matrix.package }}" = ".agents" ]; then
156156
find __tests__ -name '*.test.ts' ! -name '*.integration.test.ts' 2>/dev/null | sort | xargs -I {} bun test {} || echo "No regular tests found in .agents"
157+
elif [ "${{ matrix.package }}" = "web" ]; then
158+
bun run test --runInBand
157159
else
158160
find src -name '*.test.ts' ! -name '*.integration.test.ts' | sort | xargs -I {} bun test {}
159161
fi
@@ -244,6 +246,52 @@ jobs:
244246
find src -name '*.integration.test.ts' | sort | xargs -I {} bun test {}
245247
fi
246248
249+
# E2E tests for web using Playwright
250+
test-e2e-web:
251+
needs: [build-and-check]
252+
runs-on: ubuntu-latest
253+
steps:
254+
- name: Checkout repository
255+
uses: actions/checkout@v3
256+
257+
- name: Set up Bun
258+
uses: oven-sh/setup-bun@v2
259+
with:
260+
bun-version: '1.3.0'
261+
262+
- name: Cache dependencies
263+
uses: actions/cache@v3
264+
with:
265+
path: |
266+
node_modules
267+
*/node_modules
268+
packages/*/node_modules
269+
key: ${{ runner.os }}-deps-${{ hashFiles('**/bun.lock*') }}
270+
restore-keys: |
271+
${{ runner.os }}-deps-
272+
273+
- name: Install dependencies
274+
run: bun install --frozen-lockfile
275+
276+
- name: Install Playwright browsers
277+
run: npx playwright install --with-deps
278+
279+
- name: Set environment variables
280+
env:
281+
SECRETS_CONTEXT: ${{ toJSON(secrets) }}
282+
run: |
283+
VAR_NAMES=$(bun scripts/generate-ci-env.ts)
284+
echo "$SECRETS_CONTEXT" | jq -r --argjson vars "$VAR_NAMES" '
285+
to_entries | .[] | select(.key as $k | $vars | index($k)) | .key + "=" + .value
286+
' >> $GITHUB_ENV
287+
echo "CODEBUFF_GITHUB_ACTIONS=true" >> $GITHUB_ENV
288+
echo "NEXT_PUBLIC_CB_ENVIRONMENT=test" >> $GITHUB_ENV
289+
290+
- name: Run Playwright tests for web
291+
run: |
292+
cd web
293+
bun run e2e --reporter=list
294+
247295
# - name: Open interactive debug shell
248296
# if: ${{ failure() }}
249297
# uses: mxschmitt/action-tmate@v3

web/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default defineConfig({
2828
],
2929

3030
webServer: {
31-
command: 'bun run dev',
31+
command: 'NEXT_PUBLIC_WEB_PORT=3001 bun run dev',
3232
url: 'http://127.0.0.1:3001',
3333
reuseExistingServer: !process.env.CI,
3434
},
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test('store hydrates agents via client fetch when SSR is empty', async ({ page }) => {
4+
const agents = [
5+
{
6+
id: 'base',
7+
name: 'Base',
8+
description: 'desc',
9+
publisher: { id: 'codebuff', name: 'Codebuff', verified: true, avatar_url: null },
10+
version: '1.2.3',
11+
created_at: new Date().toISOString(),
12+
weekly_spent: 10,
13+
weekly_runs: 5,
14+
usage_count: 50,
15+
total_spent: 100,
16+
avg_cost_per_invocation: 0.2,
17+
unique_users: 3,
18+
last_used: new Date().toISOString(),
19+
version_stats: {},
20+
tags: ['test'],
21+
},
22+
]
23+
24+
// Intercept client-side fetch to /api/agents to return our fixture
25+
await page.route('**/api/agents', async (route) => {
26+
await route.fulfill({
27+
status: 200,
28+
contentType: 'application/json',
29+
body: JSON.stringify(agents),
30+
})
31+
})
32+
33+
await page.goto('/store')
34+
35+
// Expect the agent card to render after hydration by checking the copy button title
36+
await expect(
37+
page.getByTitle('Copy: codebuff --agent codebuff/base@1.2.3').first(),
38+
).toBeVisible()
39+
})

web/src/app/api/agents/route.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextResponse } from 'next/server'
22

33
import { logger } from '@/util/logger'
4+
import { applyCacheHeaders } from '@/server/apply-cache-headers'
45
import { getCachedAgents } from '@/server/agents-data'
56

67
// ISR Configuration for API route
@@ -12,19 +13,7 @@ export async function GET() {
1213
const result = await getCachedAgents()
1314

1415
const response = NextResponse.json(result)
15-
16-
// Add optimized cache headers for better performance
17-
response.headers.set(
18-
'Cache-Control',
19-
'public, max-age=300, s-maxage=600, stale-while-revalidate=3600',
20-
)
21-
22-
// Add compression and optimization headers
23-
response.headers.set('Vary', 'Accept-Encoding')
24-
response.headers.set('X-Content-Type-Options', 'nosniff')
25-
response.headers.set('Content-Type', 'application/json; charset=utf-8')
26-
27-
return response
16+
return applyCacheHeaders(response)
2817
} catch (error) {
2918
logger.error({ error }, 'Error fetching agents')
3019
return NextResponse.json(
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, it, expect } from '@jest/globals'
2+
import { applyCacheHeaders } from '../apply-cache-headers'
3+
4+
describe('applyCacheHeaders', () => {
5+
it('sets expected cache and content headers', () => {
6+
const map = new Map<string, string>()
7+
const res = { headers: { set: (k: string, v: string) => map.set(k, v) } }
8+
9+
const out = applyCacheHeaders(res)
10+
expect(out).toBe(res)
11+
expect(map.get('Cache-Control')).toContain('public')
12+
expect(map.get('Vary')).toBe('Accept-Encoding')
13+
expect(map.get('X-Content-Type-Options')).toBe('nosniff')
14+
expect(map.get('Content-Type')).toContain('application/json')
15+
})
16+
})
17+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export interface HeaderWritable {
2+
headers: { set: (k: string, v: string) => void }
3+
}
4+
5+
export function applyCacheHeaders<T extends HeaderWritable>(res: T): T {
6+
res.headers.set(
7+
'Cache-Control',
8+
'public, max-age=300, s-maxage=600, stale-while-revalidate=3600',
9+
)
10+
res.headers.set('Vary', 'Accept-Encoding')
11+
res.headers.set('X-Content-Type-Options', 'nosniff')
12+
res.headers.set('Content-Type', 'application/json; charset=utf-8')
13+
return res
14+
}
15+

0 commit comments

Comments
 (0)