@@ -102,6 +102,43 @@ async function fillNewProvider(page, dialog) {
102102 await dialog . getByLabel ( "模型名称" ) . fill ( "draft-model" ) ;
103103}
104104
105+ /**
106+ * Records what the provider picker's popup carried, in the page.
107+ *
108+ * The exit animation is a 120ms window: the popup starts leaving, the
109+ * stylesheet fades it out, and the skeleton unmounts it when the fade is
110+ * over. A reading taken across the wire can arrive after that unmount — and a
111+ * popup that is no longer in the document has no computed style to read, so
112+ * the reading comes back empty and the test fails on a state it never saw.
113+ * It can also arrive before the popup has started leaving, which reads as
114+ * the entrance animation.
115+ *
116+ * So the reading is taken where the state changes: the page watches the
117+ * popup's own class and records the popup's computed animation-name at the
118+ * moment that class changes. The record outlives the popup, so the test can
119+ * read it whenever it gets there, and it cannot miss the window.
120+ */
121+ async function recordProviderPickerMotion ( page ) {
122+ await page . evaluate ( ( ) => {
123+ const selector = '[role="listbox"][aria-label="提供商列表"]' ;
124+ window . __webuiProviderPickerMotion = [ ] ;
125+ new MutationObserver ( ( records ) => {
126+ for ( const record of records ) {
127+ const node = record . target ;
128+ if ( ! ( node instanceof Element ) || ! node . matches ( selector ) ) continue ;
129+ window . __webuiProviderPickerMotion . push ( {
130+ leaving : node . classList . contains ( "is-leaving" ) ,
131+ animationName : getComputedStyle ( node ) . animationName ,
132+ } ) ;
133+ }
134+ } ) . observe ( document . documentElement , {
135+ attributes : true ,
136+ subtree : true ,
137+ attributeFilter : [ "class" ] ,
138+ } ) ;
139+ } ) ;
140+ }
141+
105142test ( "the custom tab lists the saved provider and its model row" , async ( { page } ) => {
106143 await configureFixture ( page , seedProviderSurface ) ;
107144 await openCustomModels ( page ) ;
@@ -180,12 +217,18 @@ test.describe("with motion allowed", () => {
180217 // opens and closes the same way.
181218 expect ( await listbox . evaluate ( ( node ) => getComputedStyle ( node ) . animationName ) ) . toBe ( "webui-dropdown-in" ) ;
182219
220+ // The recorder is in place before the press, so what it records is this
221+ // press's leaving.
222+ await recordProviderPickerMotion ( page ) ;
183223 await page . mouse . click ( 6 , 6 ) ;
224+
184225 // While it is fading out it is still mounted, and it carries the exit
185- // animation rather than the entrance one.
226+ // animation rather than the entrance one. The reading is the one the page
227+ // took at the moment the popup started leaving — not a sample of a popup
228+ // that has already been unmounted.
186229 await expect
187- . poll ( ( ) => listbox . evaluate ( ( node ) => getComputedStyle ( node ) . animationName ) . catch ( ( ) => "gone" ) )
188- . toBe ( "webui-dropdown-out" ) ;
230+ . poll ( ( ) => page . evaluate ( ( ) => ( window . __webuiProviderPickerMotion ?? [ ] ) . find ( ( entry ) => entry . leaving ) ?? null ) )
231+ . toEqual ( { leaving : true , animationName : "webui-dropdown-out" } ) ;
189232 await expect ( listbox ) . toHaveCount ( 0 ) ;
190233 } ) ;
191234} ) ;
0 commit comments