Skip to content

Commit 2f1d796

Browse files
committed
test: update view_journal tests to validate iframe responses and add browser setup
1 parent 231c139 commit 2f1d796

File tree

4 files changed

+179
-16
lines changed

4 files changed

+179
-16
lines changed

exercises/03.complex/01.problem.iframe/test/index.test.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1+
import { Client } from '@modelcontextprotocol/sdk/client'
22
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
33
import { test, expect, inject } from 'vitest'
4+
import { z } from 'zod'
45

56
const mcpServerPort = inject('mcpServerPort')
67

@@ -27,10 +28,27 @@ async function setupClient() {
2728
}
2829
}
2930

30-
test('listing tools works', async () => {
31+
test('view_journal sends iframe response', async () => {
3132
await using setup = await setupClient()
3233
const { client } = setup
3334

34-
const result = await client.listTools()
35-
expect(result.tools.length).toBeGreaterThan(0)
35+
const result = await client.callTool({ name: 'view_journal' }).catch((e) => {
36+
throw new Error('🚨 view_journal tool call failed', { cause: e })
37+
})
38+
39+
const content = z.array(z.unknown()).parse(result.content)
40+
41+
expect(
42+
content,
43+
'🚨 content returned from view_journal tool does not match the expected format',
44+
).toEqual([
45+
{
46+
type: 'resource',
47+
resource: {
48+
uri: expect.stringMatching(/^ui:\/\/view-journal\/\d+$/),
49+
mimeType: 'text/uri-list',
50+
text: `http://localhost:${mcpServerPort}/ui/journal-viewer`,
51+
},
52+
},
53+
])
3654
})

exercises/03.complex/01.solution.iframe/test/index.test.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1+
import { Client } from '@modelcontextprotocol/sdk/client'
22
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
33
import { test, expect, inject } from 'vitest'
4+
import { z } from 'zod'
45

56
const mcpServerPort = inject('mcpServerPort')
67

@@ -27,10 +28,27 @@ async function setupClient() {
2728
}
2829
}
2930

30-
test('listing tools works', async () => {
31+
test('view_journal sends iframe response', async () => {
3132
await using setup = await setupClient()
3233
const { client } = setup
3334

34-
const result = await client.listTools()
35-
expect(result.tools.length).toBeGreaterThan(0)
35+
const result = await client.callTool({ name: 'view_journal' }).catch((e) => {
36+
throw new Error('🚨 view_journal tool call failed', { cause: e })
37+
})
38+
39+
const content = z.array(z.unknown()).parse(result.content)
40+
41+
expect(
42+
content,
43+
'🚨 content returned from view_journal tool does not match the expected format',
44+
).toEqual([
45+
{
46+
type: 'resource',
47+
resource: {
48+
uri: expect.stringMatching(/^ui:\/\/view-journal\/\d+$/),
49+
mimeType: 'text/uri-list',
50+
text: `http://localhost:${mcpServerPort}/ui/journal-viewer`,
51+
},
52+
},
53+
])
3654
})

exercises/03.complex/02.problem.ready/test/index.test.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1+
import { Client } from '@modelcontextprotocol/sdk/client'
22
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
3+
import { chromium } from 'playwright'
34
import { test, expect, inject } from 'vitest'
5+
import { z } from 'zod'
46

57
const mcpServerPort = inject('mcpServerPort')
68

9+
async function setupBrowser() {
10+
const browser = await chromium.launch({ headless: true })
11+
const page = await browser.newPage()
12+
return {
13+
browser,
14+
page,
15+
async [Symbol.asyncDispose]() {
16+
await browser.close()
17+
},
18+
}
19+
}
20+
721
async function setupClient() {
822
const client = new Client(
923
{
@@ -27,10 +41,58 @@ async function setupClient() {
2741
}
2842
}
2943

30-
test('listing tools works', async () => {
44+
test('view_journal sends iframe response', async () => {
3145
await using setup = await setupClient()
3246
const { client } = setup
3347

34-
const result = await client.listTools()
35-
expect(result.tools.length).toBeGreaterThan(0)
48+
const result = await client.callTool({ name: 'view_journal' }).catch((e) => {
49+
throw new Error('🚨 view_journal tool call failed', { cause: e })
50+
})
51+
52+
const content = z.array(z.object({}).passthrough()).parse(result.content)
53+
54+
expect(
55+
content,
56+
'🚨 content returned from view_journal tool does not match the expected format',
57+
).toEqual([
58+
{
59+
type: 'resource',
60+
resource: {
61+
uri: expect.stringMatching(/^ui:\/\/view-journal\/\d+$/),
62+
mimeType: 'text/uri-list',
63+
text: `http://localhost:${mcpServerPort}/ui/journal-viewer`,
64+
},
65+
},
66+
])
67+
const { resource } = z
68+
.object({ resource: z.object({ text: z.string() }) })
69+
.parse(content[0])
70+
71+
const urlString = resource.text
72+
73+
await using browserSetup = await setupBrowser()
74+
const { page } = browserSetup
75+
await page.goto(urlString)
76+
77+
const readyMessage = Promise.race([
78+
page.evaluate(() => {
79+
return new Promise<{ type: string }>((resolve) => {
80+
// @ts-expect-error - window is defined in this context
81+
window.addEventListener(
82+
'message',
83+
(event: MessageEvent) => {
84+
if (event.data?.type === 'ui-lifecycle-iframe-ready') {
85+
resolve(event.data)
86+
}
87+
},
88+
{ once: true },
89+
)
90+
})
91+
}),
92+
new Promise((r, reject) => {
93+
setTimeout(() => reject('🚨 iframe ready message not received'), 500)
94+
}),
95+
])
96+
97+
expect(await readyMessage).toEqual({ type: 'ui-lifecycle-iframe-ready' })
3698
})

exercises/03.complex/02.solution.ready/test/index.test.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1+
import { Client } from '@modelcontextprotocol/sdk/client'
22
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
3+
import { chromium } from 'playwright'
34
import { test, expect, inject } from 'vitest'
5+
import { z } from 'zod'
46

57
const mcpServerPort = inject('mcpServerPort')
68

9+
async function setupBrowser() {
10+
const browser = await chromium.launch({ headless: true })
11+
const page = await browser.newPage()
12+
return {
13+
browser,
14+
page,
15+
async [Symbol.asyncDispose]() {
16+
await browser.close()
17+
},
18+
}
19+
}
20+
721
async function setupClient() {
822
const client = new Client(
923
{
@@ -27,10 +41,61 @@ async function setupClient() {
2741
}
2842
}
2943

30-
test('listing tools works', async () => {
44+
test('view_journal sends iframe response', async () => {
3145
await using setup = await setupClient()
3246
const { client } = setup
3347

34-
const result = await client.listTools()
35-
expect(result.tools.length).toBeGreaterThan(0)
48+
const result = await client.callTool({ name: 'view_journal' }).catch((e) => {
49+
throw new Error('🚨 view_journal tool call failed', { cause: e })
50+
})
51+
52+
const content = z.array(z.object({}).passthrough()).parse(result.content)
53+
54+
expect(
55+
content,
56+
'🚨 content returned from view_journal tool does not match the expected format',
57+
).toEqual([
58+
{
59+
type: 'resource',
60+
resource: {
61+
uri: expect.stringMatching(/^ui:\/\/view-journal\/\d+$/),
62+
mimeType: 'text/uri-list',
63+
text: `http://localhost:${mcpServerPort}/ui/journal-viewer`,
64+
},
65+
},
66+
])
67+
const { resource } = z
68+
.object({ resource: z.object({ text: z.string() }) })
69+
.parse(content[0])
70+
71+
const urlString = resource.text
72+
73+
await using browserSetup = await setupBrowser()
74+
const { page } = browserSetup
75+
await page.goto(urlString)
76+
77+
const readyMessage = Promise.race([
78+
page.evaluate(() => {
79+
return new Promise<{ type: string }>((resolve) => {
80+
// @ts-expect-error - window is defined in this context
81+
window.addEventListener(
82+
'message',
83+
(event: MessageEvent) => {
84+
if (event.data?.type === 'ui-lifecycle-iframe-ready') {
85+
resolve(event.data)
86+
}
87+
},
88+
{ once: true },
89+
)
90+
})
91+
}),
92+
new Promise((r, reject) => {
93+
setTimeout(
94+
() => reject('🚨 timed out waiting for iframe ready message'),
95+
3_000,
96+
)
97+
}),
98+
])
99+
100+
expect(await readyMessage).toEqual({ type: 'ui-lifecycle-iframe-ready' })
36101
})

0 commit comments

Comments
 (0)