1- import { Client } from '@modelcontextprotocol/sdk/client/index.js'
1+ import { invariant } from '@epic-web/invariant'
2+ import { Client } from '@modelcontextprotocol/sdk/client'
23import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
4+ import { chromium , type Page } from 'playwright'
35import { test , expect , inject } from 'vitest'
6+ import { z } from 'zod'
47
58const 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+
722async 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