@@ -39,30 +39,87 @@ test.afterEach(async ({ request }) => {
3939 }
4040} ) ;
4141
42- // The /flows a11y refactor (Langflow #13891) makes `flow-name-div`
43- // `pointer-events-none`; open the flow via the card's overlay button.
44- async function reopenNewFlow ( page : Page ) : Promise < void > {
45- // Explicit timeout above the 20s default actionTimeout: this heavy spec (two
46- // bootstraps + repeated exit/re-open cycles) blew the default card-open click
47- // under CI saturation on load-degraded dailies (#790). The extra headroom
48- // absorbs transient load without masking a real regression.
49- await page
50- . getByTestId ( "list-card" )
51- . filter ( {
52- has : page . getByTestId ( "flow-name-div" ) . filter ( { hasText : "New Flow" } ) ,
53- } )
54- . getByTestId ( "list-card-open-button" )
55- . first ( )
56- . click ( { timeout : 45000 } ) ;
42+ /**
43+ * The id of the flow currently open in the editor, cross-checked against the
44+ * flows THIS page created.
45+ *
46+ * The cross-check is the point: an id in the URL that this page never created
47+ * means the spec is driving somebody else's flow — the state #1336 spent two
48+ * dailies failing on, and one better named here than six steps later as a
49+ * 45 s timeout.
50+ */
51+ async function editorFlowId ( page : Page ) : Promise < string > {
52+ const id = new URL ( page . url ( ) ) . pathname . match ( / \/ f l o w \/ ( [ 0 - 9 a - f - ] { 36 } ) / ) ?. [ 1 ] ;
53+ // The POST body is parsed asynchronously, so the entry can land a tick after
54+ // the navigation; poll briefly rather than racing it.
55+ await expect
56+ . poll ( ( ) => createdFlowIds . includes ( id ?? "" ) , { timeout : 10000 } )
57+ . toBe ( true ) ;
58+ return id ! ;
5759}
5860
59- // Quarantined: recurrent flake on the daily (2026-07-22, 2026-08-06, same
60- // signature) — reopening the just-created flow through its
61- // `list-card-open-button` times out at 45 s. Tracked in #1336; lifting the
62- // quarantine (remove `test.fixme` + restore `@stable`) is a deliverable there.
63- test . fixme (
61+ /**
62+ * Leaving the canvas is itself part of the contract under test, so assert it
63+ * instead of inferring it from whatever the next step happens to find. When the
64+ * save behind "Save And Exit" fails, the editor simply stays put — under the
65+ * old spec that surfaced 45 s later as a card-click timeout on the flows list
66+ * (#1336), which named neither the step nor the cause. Verified by forcing the
67+ * save PATCH to 500: the failure now lands on this call.
68+ */
69+ async function expectLeftEditor ( page : Page ) : Promise < void > {
70+ await page . waitForURL ( ( url ) => ! url . pathname . startsWith ( "/flow/" ) , {
71+ timeout : 30000 ,
72+ } ) ;
73+ }
74+
75+ /**
76+ * Re-open THIS test's flow by id and wait until its graph has been applied to
77+ * the canvas.
78+ *
79+ * Why by id and not through the flows-list card (#1336). The spec used to click
80+ * the first `list-card` whose name contained "New Flow". Langflow names every
81+ * blank flow "New Flow"/"New Flow (N)", so under `fullyParallel` the list holds
82+ * one per worker — and the flows list is **paginated at 12, ordered by
83+ * `updated_at DESC`**. Both halves bite:
84+ *
85+ * - the card that filter resolved was routinely ANOTHER worker's flow (proved
86+ * on nightly 1.12.0.dev18: the page created ids `8e767306` and `ee8e0ab9`
87+ * and the re-open landed on `164b3c19`, which it never created). When that
88+ * worker's own id-scoped cleanup deleted it mid-test, the save PATCH came
89+ * back 404, the editor never navigated back to the list, and the next
90+ * re-open waited out its 45 s — the exact 2026-07-22 / 2026-08-06 daily
91+ * signature, reproduced 3/8 at `--workers=4`;
92+ * - this test's own card is frequently NOT on page 1 at all (measured: 12 of
93+ * 12 slots taken by fresher flows), so no card-based selector — not even the
94+ * id-scoped `flow-name-<uuid>` testid the card carries — can find it.
95+ *
96+ * Opening by URL removes both. The trade-off is deliberate: what this spec
97+ * validates is server-side persistence across an exit/re-open, and a full
98+ * reload proves that more strictly than an SPA route change. The
99+ * "open a flow from its list card" path stays covered by the specs whose
100+ * subject it is (`bulk-actions`, `mcp-server`).
101+ */
102+ async function reopenFlow ( page : Page , flowId : string ) : Promise < void > {
103+ // The graph is applied to the canvas only after this GET resolves; waiting on
104+ // it is what keeps the `div-generic-node` counts below from reading an empty
105+ // canvas that simply had not rendered yet.
106+ const flowLoaded = page . waitForResponse (
107+ ( resp ) =>
108+ new URL ( resp . url ( ) ) . pathname === `/api/v1/flows/${ flowId } ` &&
109+ resp . request ( ) . method ( ) === "GET" &&
110+ resp . status ( ) === 200 ,
111+ { timeout : 45000 } ,
112+ ) ;
113+ await page . goto ( `/flow/${ flowId } ` ) ;
114+ await flowLoaded ;
115+ await page . waitForSelector ( '[data-testid="canvas_controls_dropdown"]' , {
116+ timeout : 45000 ,
117+ } ) ;
118+ }
119+
120+ test (
64121 "user should be able to manually save a flow when the auto_save is off" ,
65- { tag : [ "@release" , "@api" , "@database" , "@components" ] } ,
122+ { tag : [ "@stable" , "@ release", "@api" , "@database" , "@components" ] } ,
66123 async ( { page } ) => {
67124 trackCreatedFlows ( page ) ;
68125
@@ -88,7 +145,24 @@ test.fixme(
88145 timeout : 5000 ,
89146 } ) ;
90147
148+ // `awaitBootstrapTest` reaches the templates modal through the "New Flow"
149+ // entry point, which already parked the page on a freshly created
150+ // PLACEHOLDER flow — and Langflow deletes that placeholder as soon as the
151+ // modal navigates elsewhere. So the id has to be read after the blank-flow
152+ // navigation, never from the URL standing before it (#490/#681).
153+ const placeholderUrl = page . url ( ) ;
91154 await page . getByTestId ( "blank-flow" ) . click ( ) ;
155+ await page . waitForURL (
156+ ( url ) =>
157+ / \/ f l o w \/ [ 0 - 9 a - f - ] { 36 } / . test ( url . pathname ) &&
158+ url . toString ( ) !== placeholderUrl ,
159+ { timeout : 30000 } ,
160+ ) ;
161+
162+ // Resolve the flow under test once, before any edit: every re-open below is
163+ // pinned to this id, so the spec can never drive a parallel worker's
164+ // identically-named "New Flow" (#1336).
165+ const flowUnderTest = await editorFlowId ( page ) ;
92166
93167 await page . getByTestId ( "sidebar-search-input" ) . click ( ) ;
94168 await page . getByTestId ( "sidebar-search-input" ) . fill ( "chat input" ) ;
@@ -131,8 +205,9 @@ test.fixme(
131205 page . getByText ( "Unsaved changes will be permanently lost." ) ,
132206 ) . toBeVisible ( { timeout : 10000 } ) ;
133207 await page . getByText ( "Exit Anyway" , { exact : true } ) . click ( ) ;
208+ await expectLeftEditor ( page ) ;
134209
135- await reopenNewFlow ( page ) ;
210+ await reopenFlow ( page , flowUnderTest ) ;
136211
137212 await page . waitForSelector ( '[data-testid="sidebar-search-input"]' , {
138213 timeout : 5000 ,
@@ -167,8 +242,9 @@ test.fixme(
167242 const saveAndExit = page . getByText ( "Save And Exit" , { exact : true } ) . last ( ) ;
168243 await expect ( saveAndExit ) . toBeVisible ( { timeout : 10000 } ) ;
169244 await saveAndExit . click ( ) ;
245+ await expectLeftEditor ( page ) ;
170246
171- await reopenNewFlow ( page ) ;
247+ await reopenFlow ( page , flowUnderTest ) ;
172248
173249 await page . waitForSelector ( "text=loading" , {
174250 state : "hidden" ,
@@ -219,8 +295,9 @@ test.fixme(
219295 ) {
220296 await saveAndExit2 . click ( ) ;
221297 }
298+ await expectLeftEditor ( page ) ;
222299
223- await reopenNewFlow ( page ) ;
300+ await reopenFlow ( page , flowUnderTest ) ;
224301
225302 await page . waitForSelector ( '[data-testid="sidebar-search-input"]' , {
226303 timeout : 5000 ,
0 commit comments