@@ -1493,3 +1493,175 @@ describe("ChatView - Message Queueing Tests", () => {
14931493 expect ( input . getAttribute ( "data-sending-disabled" ) ) . toBe ( "false" )
14941494 } )
14951495} )
1496+
1497+ describe ( "ChatView - Cancel Button State Tests" , ( ) => {
1498+ beforeEach ( ( ) => {
1499+ vi . clearAllMocks ( )
1500+ vi . mocked ( vscode . postMessage ) . mockClear ( )
1501+ } )
1502+
1503+ it ( "resets didClickCancel state when streaming ends" , async ( ) => {
1504+ const { getByText, queryByText } = renderChatView ( )
1505+
1506+ // First hydrate state with initial task
1507+ mockPostMessage ( {
1508+ clineMessages : [
1509+ {
1510+ type : "say" ,
1511+ say : "task" ,
1512+ ts : Date . now ( ) - 2000 ,
1513+ text : "Initial task" ,
1514+ } ,
1515+ ] ,
1516+ } )
1517+
1518+ // Add a streaming API request
1519+ mockPostMessage ( {
1520+ clineMessages : [
1521+ {
1522+ type : "say" ,
1523+ say : "task" ,
1524+ ts : Date . now ( ) - 2000 ,
1525+ text : "Initial task" ,
1526+ } ,
1527+ {
1528+ type : "say" ,
1529+ say : "api_req_started" ,
1530+ ts : Date . now ( ) - 1000 ,
1531+ text : JSON . stringify ( { model : "openai-compatible" } ) ,
1532+ partial : true ,
1533+ } ,
1534+ ] ,
1535+ } )
1536+
1537+ // Wait for cancel button to appear
1538+ await waitFor ( ( ) => {
1539+ expect ( getByText ( "chat:cancel.title" ) ) . toBeInTheDocument ( )
1540+ } )
1541+
1542+ // Click cancel button
1543+ const cancelButton = getByText ( "chat:cancel.title" )
1544+ act ( ( ) => {
1545+ cancelButton . click ( )
1546+ } )
1547+
1548+ // Verify cancel task was sent
1549+ expect ( vscode . postMessage ) . toHaveBeenCalledWith ( { type : "cancelTask" } )
1550+
1551+ // Clear the mock to check for subsequent calls
1552+ vi . mocked ( vscode . postMessage ) . mockClear ( )
1553+
1554+ // Simulate streaming ending by updating messages with completed API request
1555+ mockPostMessage ( {
1556+ clineMessages : [
1557+ {
1558+ type : "say" ,
1559+ say : "task" ,
1560+ ts : Date . now ( ) - 2000 ,
1561+ text : "Initial task" ,
1562+ } ,
1563+ {
1564+ type : "say" ,
1565+ say : "api_req_started" ,
1566+ ts : Date . now ( ) - 1000 ,
1567+ text : JSON . stringify ( {
1568+ model : "openai-compatible" ,
1569+ cost : 0.01 , // Cost indicates request finished
1570+ cancelReason : "User cancelled" ,
1571+ } ) ,
1572+ partial : false ,
1573+ } ,
1574+ ] ,
1575+ } )
1576+
1577+ // Wait for UI to update
1578+ await waitFor ( ( ) => {
1579+ // Cancel button should no longer be visible when not streaming
1580+ expect ( queryByText ( "chat:cancel.title" ) ) . not . toBeInTheDocument ( )
1581+ } )
1582+
1583+ // Add a new streaming request to verify didClickCancel was reset
1584+ mockPostMessage ( {
1585+ clineMessages : [
1586+ {
1587+ type : "say" ,
1588+ say : "task" ,
1589+ ts : Date . now ( ) - 2000 ,
1590+ text : "Initial task" ,
1591+ } ,
1592+ {
1593+ type : "say" ,
1594+ say : "api_req_started" ,
1595+ ts : Date . now ( ) - 1000 ,
1596+ text : JSON . stringify ( {
1597+ model : "openai-compatible" ,
1598+ cost : 0.01 ,
1599+ cancelReason : "User cancelled" ,
1600+ } ) ,
1601+ partial : false ,
1602+ } ,
1603+ {
1604+ type : "say" ,
1605+ say : "api_req_started" ,
1606+ ts : Date . now ( ) ,
1607+ text : JSON . stringify ( { model : "openai-compatible" } ) ,
1608+ partial : true , // New streaming request
1609+ } ,
1610+ ] ,
1611+ } )
1612+
1613+ // Wait for cancel button to appear again
1614+ await waitFor ( ( ) => {
1615+ expect ( getByText ( "chat:cancel.title" ) ) . toBeInTheDocument ( )
1616+ } )
1617+
1618+ // The cancel button should be enabled
1619+ // This verifies that didClickCancel was properly reset
1620+ const newCancelButton = getByText ( "chat:cancel.title" )
1621+ // The button should not be disabled
1622+ expect ( newCancelButton ) . not . toBeDisabled ( )
1623+ } )
1624+
1625+ it ( "maintains correct button opacity based on streaming and didClickCancel state" , async ( ) => {
1626+ const { container } = renderChatView ( )
1627+
1628+ // First hydrate state with initial task
1629+ mockPostMessage ( {
1630+ clineMessages : [
1631+ {
1632+ type : "say" ,
1633+ say : "task" ,
1634+ ts : Date . now ( ) - 2000 ,
1635+ text : "Initial task" ,
1636+ } ,
1637+ ] ,
1638+ } )
1639+
1640+ // Add a streaming API request
1641+ mockPostMessage ( {
1642+ clineMessages : [
1643+ {
1644+ type : "say" ,
1645+ say : "task" ,
1646+ ts : Date . now ( ) - 2000 ,
1647+ text : "Initial task" ,
1648+ } ,
1649+ {
1650+ type : "say" ,
1651+ say : "api_req_started" ,
1652+ ts : Date . now ( ) - 1000 ,
1653+ text : JSON . stringify ( { model : "openai-compatible" } ) ,
1654+ partial : true ,
1655+ } ,
1656+ ] ,
1657+ } )
1658+
1659+ // Wait for button container to appear
1660+ await waitFor ( ( ) => {
1661+ const buttonContainer = container . querySelector ( '[class*="opacity-"]' )
1662+ expect ( buttonContainer ) . toBeInTheDocument ( )
1663+ // Should have opacity-100 when streaming and cancel not clicked
1664+ expect ( buttonContainer ?. className ) . toContain ( "opacity-100" )
1665+ } )
1666+ } )
1667+ } )
0 commit comments