Skip to content

Commit 08f7983

Browse files
committed
test: implement journal viewer test to validate prompt message handling and improve browser setup
1 parent a4c969e commit 08f7983

File tree

2 files changed

+170
-10
lines changed

2 files changed

+170
-10
lines changed

exercises/04.interactive/03.problem.prompts/test/index.test.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1+
import { invariant } from '@epic-web/invariant'
2+
import { Client } from '@modelcontextprotocol/sdk/client'
23
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
4+
import { chromium, type Page } from 'playwright'
35
import { test, expect, inject } from 'vitest'
6+
import { z } from 'zod'
47

58
const mcpServerPort = inject('mcpServerPort')
69

10+
async function setupBrowser() {
11+
const browser = await chromium.launch({ headless: false })
12+
const page = await browser.newPage()
13+
return {
14+
browser,
15+
page,
16+
async [Symbol.asyncDispose]() {
17+
await browser.close()
18+
},
19+
}
20+
}
21+
722
async function setupClient() {
823
const client = new Client(
924
{
@@ -27,10 +42,75 @@ async function setupClient() {
2742
}
2843
}
2944

30-
test('listing tools works', async () => {
45+
test('journal viewer sends prompt message', async () => {
3146
await using setup = await setupClient()
3247
const { client } = setup
3348

34-
const result = await client.listTools()
35-
expect(result.tools.length).toBeGreaterThan(0)
36-
})
49+
const result = await client.callTool({ name: 'view_journal' }).catch((e) => {
50+
throw new Error('🚨 view_journal tool call failed', { cause: e })
51+
})
52+
53+
invariant(Array.isArray(result.content), '🚨 content is not an array')
54+
55+
const { resource } = z
56+
.object({ resource: z.object({}).passthrough() })
57+
.parse(result.content[0])
58+
59+
const url = new URL('http://localhost:7787/mcp-ui-renderer')
60+
url.searchParams.set('resourceData', JSON.stringify(resource))
61+
62+
await using browserSetup = await setupBrowser()
63+
const { page } = browserSetup
64+
65+
await page.goto(url.toString())
66+
67+
await handleViteDeps(page)
68+
69+
const iframe = page.frameLocator('iframe')
70+
71+
const viewDetailsButton = iframe
72+
.getByRole('button', { name: 'Summarize' })
73+
.first()
74+
await viewDetailsButton.click()
75+
76+
const message = page.getByRole('log').getByText('prompt')
77+
await message.waitFor({ timeout: 10_000 }).catch((e) => {
78+
throw new Error(
79+
'🚨 prompt message was never received. Make sure to call sendMcpMessage with "prompt"',
80+
{ cause: e },
81+
)
82+
})
83+
84+
const textContent = await message.textContent()
85+
const messageContent = JSON.parse(textContent!)
86+
expect(
87+
messageContent,
88+
'🚨 the prompt message is not the correct format',
89+
).toEqual({
90+
type: 'prompt',
91+
messageId: expect.any(String),
92+
payload: {
93+
prompt: expect.any(String),
94+
},
95+
})
96+
// then click the "send" button
97+
// no need to input anything in this case because there's no expected response
98+
await page.getByRole('button', { name: 'send' }).click()
99+
}, 50_000)
100+
101+
// because vite needs to optimize deps 😭😡
102+
async function handleViteDeps(page: Page) {
103+
await page
104+
.frameLocator('iframe')
105+
.locator('vite-error-overlay')
106+
.waitFor({ timeout: 200 })
107+
.then(
108+
async () => {
109+
await page.reload()
110+
await new Promise((resolve) => setTimeout(resolve, 400))
111+
},
112+
() => {
113+
// good...
114+
},
115+
)
116+
}

exercises/04.interactive/03.solution.prompts/test/index.test.ts

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1+
import { invariant } from '@epic-web/invariant'
2+
import { Client } from '@modelcontextprotocol/sdk/client'
23
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
4+
import { chromium, type Page } from 'playwright'
35
import { test, expect, inject } from 'vitest'
6+
import { z } from 'zod'
47

58
const mcpServerPort = inject('mcpServerPort')
69

10+
async function setupBrowser() {
11+
const browser = await chromium.launch({ headless: false })
12+
const page = await browser.newPage()
13+
return {
14+
browser,
15+
page,
16+
async [Symbol.asyncDispose]() {
17+
await browser.close()
18+
},
19+
}
20+
}
21+
722
async function setupClient() {
823
const client = new Client(
924
{
@@ -27,10 +42,75 @@ async function setupClient() {
2742
}
2843
}
2944

30-
test('listing tools works', async () => {
45+
test('journal viewer sends prompt message', async () => {
3146
await using setup = await setupClient()
3247
const { client } = setup
3348

34-
const result = await client.listTools()
35-
expect(result.tools.length).toBeGreaterThan(0)
36-
})
49+
const result = await client.callTool({ name: 'view_journal' }).catch((e) => {
50+
throw new Error('🚨 view_journal tool call failed', { cause: e })
51+
})
52+
53+
invariant(Array.isArray(result.content), '🚨 content is not an array')
54+
55+
const { resource } = z
56+
.object({ resource: z.object({}).passthrough() })
57+
.parse(result.content[0])
58+
59+
const url = new URL('http://localhost:7787/mcp-ui-renderer')
60+
url.searchParams.set('resourceData', JSON.stringify(resource))
61+
62+
await using browserSetup = await setupBrowser()
63+
const { page } = browserSetup
64+
65+
await page.goto(url.toString())
66+
67+
await handleViteDeps(page)
68+
69+
const iframe = page.frameLocator('iframe')
70+
71+
const viewDetailsButton = iframe
72+
.getByRole('button', { name: 'Summarize' })
73+
.first()
74+
await viewDetailsButton.click()
75+
76+
const message = page.getByRole('log').getByText('prompt')
77+
await message.waitFor({ timeout: 10_000 }).catch((e) => {
78+
throw new Error(
79+
'🚨 prompt message was never received. Make sure to call sendMcpMessage with "prompt"',
80+
{ cause: e },
81+
)
82+
})
83+
84+
const textContent = await message.textContent()
85+
const messageContent = JSON.parse(textContent!)
86+
expect(
87+
messageContent,
88+
'🚨 the prompt message is not the correct format',
89+
).toEqual({
90+
type: 'prompt',
91+
messageId: expect.any(String),
92+
payload: {
93+
prompt: expect.any(String),
94+
},
95+
})
96+
// then click the "send" button
97+
// no need to input anything in this case because there's no expected response
98+
await page.getByRole('button', { name: 'send' }).click()
99+
}, 50_000)
100+
101+
// because vite needs to optimize deps 😭😡
102+
async function handleViteDeps(page: Page) {
103+
await page
104+
.frameLocator('iframe')
105+
.locator('vite-error-overlay')
106+
.waitFor({ timeout: 200 })
107+
.then(
108+
async () => {
109+
await page.reload()
110+
await new Promise((resolve) => setTimeout(resolve, 400))
111+
},
112+
() => {
113+
// good...
114+
},
115+
)
116+
}

0 commit comments

Comments
 (0)