66// was no longer listed or watchable anywhere. The Terminal owner now holds that
77// truth, and process lifetime belongs to `disposeProcess` / `disposeAll`.
88//
9+ // A terminal is opened for a workspace tab, and the runtime records that
10+ // binding: the tab that asked for the PTY is the tab that finds it again in
11+ // the roster. Two pages that open the same terminal tab therefore meet the
12+ // same process, and the panel that goes away releases its watch without
13+ // ending it.
14+ //
915// A unit test can prove the owner keeps a record, but only the built client can
1016// prove what the row is actually about: that a PTY created while session A was
1117// on screen is still listed, still watched and still producing output after the
@@ -32,20 +38,14 @@ const openTerminalTab = async (page) => {
3238 await expect ( page . getByTestId ( "workspace-panel" ) ) . toHaveAttribute ( "data-active-tab" , "terminal" ) ;
3339} ;
3440
35- const createTerminal = async ( page ) => {
36- await page . getByRole ( "button" , { name : "新建终端" } ) . click ( ) ;
37- await expect
38- . poll ( ( ) => page . evaluate ( ( ) => window . __fixture . terminals ( ) . length ) )
39- . toBeGreaterThan ( 0 ) ;
40- } ;
41-
4241test ( "a PTY created on one page is listed and watched from another" , async ( { page } ) => {
4342 await configureFixture ( page , ( ) => undefined ) ;
4443 await openApp ( page ) ;
4544
4645 await openTerminalTab ( page ) ;
47- await createTerminal ( page ) ;
48-
46+ // The tab asked the runtime for a terminal, and the runtime made one: the
47+ // process belongs to the runtime, not to the tab that opened it.
48+ await expect . poll ( ( ) => page . evaluate ( ( ) => window . __fixture . terminals ( ) . length ) ) . toBe ( 1 ) ;
4949 const created = await page . evaluate ( ( ) => window . __fixture . terminals ( ) ) ;
5050 expect ( created ) . toHaveLength ( 1 ) ;
5151 const { terminalId } = created [ 0 ] ;
@@ -61,7 +61,9 @@ test("a PTY created on one page is listed and watched from another", async ({ pa
6161 await configureFixture ( pageB , ( ) => undefined ) ;
6262 await openApp ( pageB ) ;
6363 await openTerminalTab ( pageB ) ;
64- await expect ( pageB . getByTestId ( "workspace-panel" ) ) . toContainText ( "#1" ) ;
64+ // The catalogue the runtime answers is the runtime's: it still holds the PTY
65+ // the first page opened, and the second page adopts that process rather than
66+ // asking for another one.
6567 await expect
6668 . poll ( ( ) => pageB . evaluate ( ( ) => window . __fixture . watchedTerminalIds ( ) ) )
6769 . toContain ( terminalId ) ;
@@ -84,7 +86,7 @@ test("a PTY created on one page is listed and watched from another", async ({ pa
8486 expect ( await page . evaluate ( ( ) => window . __fixture . terminals ( ) [ 0 ] ?. status ) ) . toBe ( "running" ) ;
8587
8688 // The roster holds exactly the PTY created before the round trip. A fixture
87- // or an owner that re-created on attach would show two tabs here.
89+ // or an owner that re-created on attach would show two rows here.
8890 const final = await page . evaluate ( ( ) => window . __fixture . terminals ( ) ) ;
8991 expect ( final ) . toHaveLength ( 1 ) ;
9092 expect ( final [ 0 ] . terminalId ) . toBe ( terminalId ) ;
@@ -97,15 +99,14 @@ test("a PTY created on one page is listed and watched from another", async ({ pa
9799 expect ( delivered ) . toBe ( 1 ) ;
98100} ) ;
99101
100- test ( "a watch is released without ending the process, and a disposed PTY does not come back" , async ( {
102+ test ( "a watch is released without ending the process, and an ended PTY does not come back" , async ( {
101103 page,
102104} ) => {
103105 await configureFixture ( page , ( ) => undefined ) ;
104106 await openApp ( page ) ;
105107
106108 await openTerminalTab ( page ) ;
107- await createTerminal ( page ) ;
108-
109+ await expect . poll ( ( ) => page . evaluate ( ( ) => window . __fixture . terminals ( ) . length ) ) . toBe ( 1 ) ;
109110 const { terminalId } = ( await page . evaluate ( ( ) => window . __fixture . terminals ( ) ) ) [ 0 ] ;
110111
111112 // Closing the panel detaches the watch. The row is about the two halves being
@@ -119,15 +120,16 @@ test("a watch is released without ending the process, and a disposed PTY does no
119120 expect ( stillRunning ) . toHaveLength ( 1 ) ;
120121 expect ( stillRunning [ 0 ] . status ) . toBe ( "running" ) ;
121122
122- // An explicit close is the other half: it ends the process, and the roster
123- // loses the row rather than reporting it as exited-but-present.
124- await openTerminalTab ( page ) ;
125- await page . locator ( ".webui-terminal-actions" ) . getByRole ( "button" , { name : "关闭" } ) . click ( ) ;
126-
127- await expect
128- . poll ( ( ) => page . evaluate ( ( ) => window . __fixture . terminals ( ) . length ) )
129- . toBe ( 0 ) ;
130- await expect
131- . poll ( ( ) => page . evaluate ( ( ) => window . __fixture . watchedTerminalIds ( ) ) )
132- . not . toContain ( terminalId ) ;
123+ // The other half: the process ends on the runtime's side. The roster keeps
124+ // the row, so the client still knows that tab's terminal, and it must not
125+ // answer the ending by opening another process for the same tab.
126+ const creates = ( ) => page . evaluate ( ( ) => window . __fixture . requests . filter (
127+ ( request ) => request . operation === "createTerminal" ,
128+ ) . length ) ;
129+ const before = await creates ( ) ;
130+ expect ( await page . evaluate ( ( id ) => window . __fixture . exitTerminal ( id ) , terminalId ) ) . toBe ( true ) ;
131+ await page . getByTestId ( "workspace-panel-controls" ) . getByRole ( "button" , { name : "浏览器" } ) . click ( ) ;
132+ await expect ( page . getByTestId ( "workspace-panel" ) ) . toHaveAttribute ( "data-active-tab" , "terminal" ) ;
133+ await expect . poll ( ( ) => page . evaluate ( ( ) => window . __fixture . terminals ( ) [ 0 ] ?. status ) ) . toBe ( "exited" ) ;
134+ expect ( await creates ( ) ) . toBe ( before ) ;
133135} ) ;
0 commit comments