1+ import { DatabaseSync } from "node:sqlite" ;
12import { expect , test } from "@playwright/test" ;
2- import { goto , toast , undoButton } from "./helpers" ;
3+ import { activeProjectDbFile , goto , toast , undoButton , uniqueName } from "./helpers" ;
34
45test ( "what-if target stays editable while the factory re-solves" , async ( { page } ) => {
56 await goto ( page , "/factory/scenario" ) ;
@@ -13,7 +14,7 @@ test("what-if target stays editable while the factory re-solves", async ({ page
1314 // preserving both the in-progress value and keyboard focus throughout them.
1415 await expect ( firstDemand ) . toBeFocused ( ) ;
1516 await expect ( firstDemand ) . toHaveValue ( "12.34" ) ;
16- await expect ( page . getByText ( "Block changes" , { exact : false } ) . first ( ) ) . toBeVisible ( {
17+ await expect ( page . getByText ( "Goal changes" , { exact : false } ) . first ( ) ) . toBeVisible ( {
1718 timeout : 15_000 ,
1819 } ) ;
1920 await expect ( firstDemand ) . toBeFocused ( ) ;
@@ -52,13 +53,14 @@ test("what-if 'apply all' re-balances the factory in one undoable step", async (
5253 // confirm dialog summarizes the change; commit it
5354 const dialog = page . getByRole ( "dialog" , { name : / R e - b a l a n c e t h e w h o l e f a c t o r y / } ) ;
5455 await expect ( dialog ) . toBeVisible ( ) ;
55- await dialog . getByRole ( "button" , { name : / ^ A p p l y t o \d + b l o c k / } ) . click ( ) ;
56+ await dialog . getByRole ( "button" , { name : / ^ A p p l y \d + c h a n g e / } ) . click ( ) ;
5657 // the apply iterates the solve to convergence across every touched block, so the
5758 // dialog can sit in its "applying…" state for a while before it closes
5859 await expect ( dialog ) . toBeHidden ( { timeout : 45_000 } ) ;
5960
60- // the whole batch reports as one action, with an Undo affordance
61- await expect ( toast ( page , / R e - b a l a n c e d \d + b l o c k / ) ) . toBeVisible ( { timeout : 30_000 } ) ;
61+ // the whole batch reports as one undoable action. The success toast is brief
62+ // and can expire while the final query invalidations settle; the undo label is
63+ // the durable proof that the write completed.
6264 await expect ( undoButton ( page ) ) . toHaveAccessibleName ( / U n d o : R e - b a l a n c e f a c t o r y / , {
6365 timeout : 15_000 ,
6466 } ) ;
@@ -70,4 +72,89 @@ test("what-if 'apply all' re-balances the factory in one undoable step", async (
7072 await expect ( undoButton ( page ) ) . toBeEnabled ( ) ;
7173 await undoButton ( page ) . click ( ) ;
7274 await expect ( toast ( page , / U n d i d : R e - b a l a n c e f a c t o r y / ) ) . toBeVisible ( { timeout : 30_000 } ) ;
75+
76+ } ) ;
77+
78+ test ( "Scenario applies a secondary consume goal independently" , async ( { page } ) => {
79+ test . setTimeout ( 120_000 ) ;
80+ // Resolve the copied project's existing projections before inserting the two
81+ // self-contained test blocks and their cached factory boundary flows.
82+ await goto ( page , "/factory/scenario" ) ;
83+
84+ const sourceName = uniqueName ( "Acetaldehyde surplus" ) ;
85+ const sinkName = uniqueName ( "Secondary sink" ) ;
86+ const db = new DatabaseSync ( activeProjectDbFile ( ) ) ;
87+ let sourceId : number ;
88+ let sinkId : number ;
89+ try {
90+ db . exec ( "PRAGMA busy_timeout = 5000" ) ;
91+ sourceId = Number (
92+ db
93+ . prepare ( "INSERT INTO blocks (name, data, solve_status) VALUES (?, ?, 'solved')" )
94+ . run (
95+ sourceName ,
96+ JSON . stringify ( { goals : [ { name : "water" , rate : 1 } ] , recipes : [ ] } ) ,
97+ ) . lastInsertRowid ,
98+ ) ;
99+ sinkId = Number (
100+ db
101+ . prepare ( "INSERT INTO blocks (name, data, solve_status) VALUES (?, ?, 'solved')" )
102+ . run (
103+ sinkName ,
104+ JSON . stringify ( {
105+ goals : [
106+ { name : "water" , rate : - 1 } ,
107+ { name : "acetaldehyde" , rate : - 2 } ,
108+ ] ,
109+ recipes : [ "spoil-acetaldehyde" ] ,
110+ } ) ,
111+ ) . lastInsertRowid ,
112+ ) ;
113+ const insertFlow = db . prepare (
114+ "INSERT INTO block_flows (block_id, item, kind, role, rate) VALUES (?, ?, ?, ?, ?)" ,
115+ ) ;
116+ insertFlow . run ( sourceId , "acetaldehyde" , "item" , "byproduct" , 10 ) ;
117+ insertFlow . run ( sinkId , "acetaldehyde" , "item" , "import" , 2 ) ;
118+ } finally {
119+ db . close ( ) ;
120+ }
121+
122+ await goto ( page , "/factory/scenario" ) ;
123+ const change = page . getByRole ( "link" , { name : / ^ A c e t a l d e h y d e / } ) ;
124+ await expect ( change ) . toContainText ( "-2" ) ;
125+ await expect ( change ) . toContainText ( "-10" ) ;
126+
127+ await page . getByRole ( "button" , { name : "Apply all" } ) . click ( ) ;
128+ const dialog = page . getByRole ( "dialog" , { name : / R e - b a l a n c e t h e w h o l e f a c t o r y / } ) ;
129+ await dialog . getByRole ( "button" , { name : / ^ A p p l y \d + c h a n g e / } ) . click ( ) ;
130+ await expect ( dialog ) . toBeHidden ( { timeout : 45_000 } ) ;
131+ await expect ( toast ( page , / R e - b a l a n c e d \d + b l o c k / ) ) . toBeVisible ( { timeout : 15_000 } ) ;
132+ await expect ( undoButton ( page ) ) . toHaveAccessibleName ( / U n d o : R e - b a l a n c e f a c t o r y / , {
133+ timeout : 15_000 ,
134+ } ) ;
135+
136+ const saved = new DatabaseSync ( activeProjectDbFile ( ) , { readOnly : true } ) ;
137+ try {
138+ const row = saved . prepare ( "SELECT data FROM blocks WHERE id = ?" ) . get ( sinkId ) as {
139+ data : string ;
140+ } ;
141+ const doc = JSON . parse ( row . data ) as { goals : { name : string ; rate : number } [ ] } ;
142+ expect ( doc . goals . find ( ( goal ) => goal . name === "acetaldehyde" ) ?. rate ) . toBe ( - 10 ) ;
143+ expect ( doc . goals . find ( ( goal ) => goal . name === "water" ) ?. rate ) . toBe ( - 1 ) ;
144+ } finally {
145+ saved . close ( ) ;
146+ }
147+
148+ await undoButton ( page ) . click ( ) ;
149+ await expect ( toast ( page , / U n d i d : R e - b a l a n c e f a c t o r y / ) ) . toBeVisible ( { timeout : 30_000 } ) ;
150+
151+ const cleanup = new DatabaseSync ( activeProjectDbFile ( ) ) ;
152+ try {
153+ cleanup . exec ( "PRAGMA busy_timeout = 5000" ) ;
154+ cleanup . prepare ( "DELETE FROM block_flows WHERE block_id IN (?, ?)" ) . run ( sourceId , sinkId ) ;
155+ cleanup . prepare ( "DELETE FROM block_machines WHERE block_id IN (?, ?)" ) . run ( sourceId , sinkId ) ;
156+ cleanup . prepare ( "DELETE FROM blocks WHERE id IN (?, ?)" ) . run ( sourceId , sinkId ) ;
157+ } finally {
158+ cleanup . close ( ) ;
159+ }
73160} ) ;
0 commit comments