@@ -2,7 +2,6 @@ import { test, expect } from '../support/world';
22import { sel } from '../support/selectors' ;
33
44const CHAT_POST_TIMEOUT_MS = 8_000 ;
5- const AGENT_REPLY_TIMEOUT_MS = 5_000 ;
65
76// US-16: Message Deletion from Timeline
87//
@@ -30,55 +29,49 @@ function deleteButtonFor(page: import('@playwright/test').Page, postSelector: st
3029 return page . locator ( `${ postSelector } .post-delete-btn, ${ postSelector } [aria-label="Delete message"]` ) ;
3130}
3231
33- /** Send a message and wait for the authenticated chat POST plus timeline echo. */
34- async function sendMessage ( page : import ( '@playwright/test' ) . Page , text : string ) {
35- const userPostCount = await page . locator ( '.post:not(.agent-post)' ) . count ( ) ;
36- const responsePromise = page . waitForResponse (
37- ( response ) => response . url ( ) . includes ( '/agent/chat' ) && response . request ( ) . method ( ) === 'POST' ,
38- { timeout : CHAT_POST_TIMEOUT_MS } ,
39- ) . catch ( ( ) => null ) ;
40-
41- const compose = page . locator ( sel . composeInput ) ;
42- await compose . click ( ) ;
43- await compose . fill ( text ) ;
44- await page . keyboard . press ( 'Enter' ) ;
45-
46- const response = await responsePromise ;
47- if ( ! response ) {
48- throw new Error ( `No /agent/chat response observed for ${ JSON . stringify ( text ) } ; E2E auth/session bootstrap may be missing.` ) ;
49- }
50- if ( ! response . ok ( ) ) {
51- throw new Error ( `/agent/chat failed with HTTP ${ response . status ( ) } for ${ JSON . stringify ( text ) } : ${ await response . text ( ) . catch ( ( ) => '' ) } ` ) ;
52- }
53-
54- await page . waitForFunction (
55- ( { previousCount, expectedText } ) => {
56- const userPosts = Array . from ( document . querySelectorAll ( '.post:not(.agent-post)' ) ) ;
57- return userPosts . length > previousCount && userPosts . some ( ( post ) => post . textContent ?. includes ( expectedText ) ) ;
58- } ,
59- { previousCount : userPostCount , expectedText : text } ,
60- { timeout : CHAT_POST_TIMEOUT_MS } ,
61- ) ;
32+ /** Create a timeline post directly so deletion tests do not leave the agent busy. */
33+ async function sendMessage ( page : import ( '@playwright/test' ) . Page , text : string ) : Promise < number > {
34+ const response = await page . evaluate ( async ( content ) => {
35+ const res = await fetch ( '/post' , {
36+ method : 'POST' ,
37+ headers : { 'Content-Type' : 'application/json' } ,
38+ body : JSON . stringify ( { content } ) ,
39+ } ) ;
40+ const body = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
41+ if ( ! res . ok ) throw new Error ( `/post failed with HTTP ${ res . status } : ${ JSON . stringify ( body ) } ` ) ;
42+ return body ;
43+ } , text ) ;
44+
45+ const id = Number ( ( response as { id ?: unknown } ) . id ) ;
46+ await expect ( page . locator ( `#post-${ id } ` ) ) . toBeVisible ( { timeout : CHAT_POST_TIMEOUT_MS } ) ;
47+ return id ;
6248}
6349
64- /** Wait briefly for an agent response; deletion tests can continue/skip cascade checks without one. */
65- async function waitForAgentReply ( page : import ( '@playwright/test' ) . Page , timeoutMs = AGENT_REPLY_TIMEOUT_MS ) {
66- const postCount = await page . locator ( sel . post ) . count ( ) ;
67- await page . waitForFunction (
68- ( count ) => document . querySelectorAll ( '.post' ) . length > count ,
69- postCount ,
70- { timeout : timeoutMs }
71- ) . catch ( ( ) => { } ) ;
72- await page . waitForTimeout ( 500 ) ;
50+ /** Create a deterministic thread reply without invoking the agent. */
51+ async function sendReply ( page : import ( '@playwright/test' ) . Page , threadId : number , text : string ) : Promise < number > {
52+ const response = await page . evaluate ( async ( { threadId, content } ) => {
53+ const res = await fetch ( '/post/reply' , {
54+ method : 'POST' ,
55+ headers : { 'Content-Type' : 'application/json' } ,
56+ body : JSON . stringify ( { thread_id : threadId , content } ) ,
57+ } ) ;
58+ const body = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
59+ if ( ! res . ok ) throw new Error ( `/post/reply failed with HTTP ${ res . status } : ${ JSON . stringify ( body ) } ` ) ;
60+ return body ;
61+ } , { threadId, content : text } ) ;
62+
63+ const id = Number ( ( response as { id ?: unknown } ) . id ) ;
64+ await expect ( page . locator ( `#post-${ id } ` ) ) . toBeVisible ( { timeout : CHAT_POST_TIMEOUT_MS } ) ;
65+ return id ;
7366}
7467
7568// ── Single message deletion ──────────────────────────────────────
7669
7770test . describe ( 'US-16: Single Message Deletion' , ( ) => {
7871 test ( 'delete button visible on hover' , async ( { authedPage : page } ) => {
79- await page . waitForSelector ( sel . post ) ;
72+ const postId = await sendMessage ( page , `hover-delete-test- ${ Date . now ( ) } ` ) ;
8073
81- const post = page . locator ( sel . post ) . last ( ) ;
74+ const post = page . locator ( `# post- ${ postId } ` ) ;
8275 await post . hover ( ) ;
8376 await page . waitForTimeout ( 300 ) ;
8477
@@ -89,7 +82,6 @@ test.describe('US-16: Single Message Deletion', () => {
8982 test ( 'delete a single message removes it from timeline' , async ( { authedPage : page } ) => {
9083 // Send a test message
9184 await sendMessage ( page , `e2e-delete-test-${ Date . now ( ) } ` ) ;
92- await waitForAgentReply ( page ) ;
9385
9486 // Get the user message we just sent
9587 const userPosts = page . locator ( '.post:not(.agent-post)' ) ;
@@ -140,7 +132,7 @@ test.describe('US-16: Single Message Deletion', () => {
140132 }
141133
142134 // Refresh
143- await page . reload ( { waitUntil : 'networkidle ' } ) ;
135+ await page . reload ( { waitUntil : 'domcontentloaded ' } ) ;
144136 await page . waitForSelector ( sel . post , { timeout : 10000 } ) . catch ( ( ) => { } ) ;
145137 await page . waitForTimeout ( 1000 ) ;
146138
@@ -154,9 +146,9 @@ test.describe('US-16: Single Message Deletion', () => {
154146
155147test . describe ( 'US-16: Cascading Thread Deletion' , ( ) => {
156148 test ( 'deleting a parent with replies shows confirmation with count' , async ( { authedPage : page } ) => {
157- // Send a message that will get a reply (agent response creates a thread)
158- await sendMessage ( page , 'Reply to this for thread deletion test' ) ;
159- await waitForAgentReply ( page ) ;
149+ // Create a parent plus deterministic replies without invoking the agent.
150+ const parentId = await sendMessage ( page , 'Reply to this for thread deletion test' ) ;
151+ await sendReply ( page , parentId , 'Thread deletion reply 1' ) ;
160152
161153 // The user message should now have at least 1 reply (the agent response)
162154 // Find the user message (parent)
@@ -167,29 +159,9 @@ test.describe('US-16: Cascading Thread Deletion', () => {
167159 return ;
168160 }
169161
170- const parentPost = userPosts . last ( ) ;
171-
172- // Check if this post has thread replies visible
173- const parentId = await parentPost . evaluate ( ( el ) => {
174- const idAttr = el . id ?. replace ( 'post-' , '' ) ;
175- return idAttr || null ;
176- } ) ;
177-
178- if ( ! parentId ) {
179- test . skip ( undefined , 'Cannot determine parent post ID' ) ;
180- return ;
181- }
182-
183- // Count visible replies
184- const replyCount = await page . evaluate ( ( pid ) => {
185- return document . querySelectorAll ( `.post.thread-reply` ) . length ;
186- } , parentId ) ;
162+ const parentPost = page . locator ( `#post-${ parentId } ` ) ;
187163
188- if ( replyCount === 0 ) {
189- // Agent may not have threaded its reply — skip cascade-specific test
190- test . skip ( undefined , 'No thread replies to test cascade' ) ;
191- return ;
192- }
164+ await expect ( page . locator ( sel . postContent , { hasText : 'Thread deletion reply 1' } ) ) . toBeVisible ( ) ;
193165
194166 // Set up dialog listener to check the message
195167 let dialogMessage = '' ;
@@ -209,15 +181,13 @@ test.describe('US-16: Cascading Thread Deletion', () => {
209181 } ) ;
210182
211183 test ( 'confirming cascade removes parent and all replies' , async ( { authedPage : page } ) => {
212- await sendMessage ( page , 'Cascade delete test - please reply' ) ;
213- await waitForAgentReply ( page ) ;
184+ const parentId = await sendMessage ( page , 'Cascade delete test - please reply' ) ;
185+ await sendReply ( page , parentId , 'Cascade delete reply 1' ) ;
214186
215187 const postsBefore = await getVisiblePostIds ( page ) ;
216188 const countBefore = postsBefore . length ;
217189
218- // Find the last user post
219- const userPosts = page . locator ( '.post:not(.agent-post)' ) ;
220- const parentPost = userPosts . last ( ) ;
190+ const parentPost = page . locator ( `#post-${ parentId } ` ) ;
221191
222192 // Accept cascade confirmation
223193 page . on ( 'dialog' , async ( dialog ) => {
@@ -238,13 +208,12 @@ test.describe('US-16: Cascading Thread Deletion', () => {
238208 } ) ;
239209
240210 test ( 'cancelling cascade preserves all messages' , async ( { authedPage : page } ) => {
241- await sendMessage ( page , 'Cancel cascade test' ) ;
242- await waitForAgentReply ( page ) ;
211+ const parentId = await sendMessage ( page , 'Cancel cascade test' ) ;
212+ await sendReply ( page , parentId , 'Cancel cascade reply 1' ) ;
243213
244214 const postsBefore = await getVisiblePostIds ( page ) ;
245215
246- const userPosts = page . locator ( '.post:not(.agent-post)' ) ;
247- const parentPost = userPosts . last ( ) ;
216+ const parentPost = page . locator ( `#post-${ parentId } ` ) ;
248217
249218 // Dismiss (cancel) confirmation
250219 page . on ( 'dialog' , async ( dialog ) => {
@@ -265,12 +234,11 @@ test.describe('US-16: Cascading Thread Deletion', () => {
265234 } ) ;
266235
267236 test ( 'no orphaned replies remain after cascade delete' , async ( { authedPage : page } ) => {
268- await sendMessage ( page , 'Orphan check test' ) ;
269- await waitForAgentReply ( page ) ;
237+ const createdParentId = await sendMessage ( page , 'Orphan check test' ) ;
238+ await sendReply ( page , createdParentId , 'Orphan check reply 1' ) ;
270239
271- const userPosts = page . locator ( '.post:not(.agent-post)' ) ;
272- const parentPost = userPosts . last ( ) ;
273- const parentId = await parentPost . evaluate ( el => el . id ?. replace ( 'post-' , '' ) || '' ) ;
240+ const parentPost = page . locator ( `#post-${ createdParentId } ` ) ;
241+ const parentId = String ( createdParentId ) ;
274242
275243 // Accept cascade
276244 page . on ( 'dialog' , async ( dialog ) => await dialog . accept ( ) ) ;
0 commit comments