@@ -12,6 +12,18 @@ async function dismissDataDriftPrompt(page: Page) {
1212 }
1313}
1414
15+ async function recalculateScenario ( page : Page ) {
16+ const status = page . getByTestId ( "scenario-status" ) ;
17+ await expect ( status ) . toBeVisible ( { timeout : 30_000 } ) ;
18+ await expect ( status ) . toHaveAttribute ( "data-state" , / ^ ( c u r r e n t | s t a l e | e m p t y | c a l c u l a t i n g ) $ / , {
19+ timeout : 30_000 ,
20+ } ) ;
21+ const state = await status . getAttribute ( "data-state" ) ;
22+ if ( state === "stale" )
23+ await status . getByRole ( "button" , { name : "Recalculate" } ) . click ( ) ;
24+ await expect ( status ) . toHaveAttribute ( "data-state" , "current" , { timeout : 90_000 } ) ;
25+ }
26+
1527test ( "factory pins can add and persist a consumption target" , async ( { page } ) => {
1628 await goto ( page , "/factory/scenario" ) ;
1729 await dismissDataDriftPrompt ( page ) ;
@@ -24,8 +36,11 @@ test("factory pins can add and persist a consumption target", async ({ page }) =
2436 const input = page . getByRole ( "spinbutton" , { name : "Acetaldehyde factory pin" } ) ;
2537 await expect ( input ) . toBeVisible ( ) ;
2638 await input . fill ( "-2" ) ;
27- await input . blur ( ) ;
2839 await expect ( input ) . toHaveValue ( "-2" ) ;
40+ await page . getByTestId ( "scenario-status" ) . getByRole ( "button" , { name : "Recalculate" } ) . click ( ) ;
41+ await expect ( page . getByTestId ( "scenario-status" ) ) . toHaveAttribute ( "data-state" , "current" , {
42+ timeout : 90_000 ,
43+ } ) ;
2944 await expect
3045 . poll ( ( ) => {
3146 const db = new DatabaseSync ( activeProjectDbFile ( ) , { readOnly : true } ) ;
@@ -47,24 +62,55 @@ test("factory pins can add and persist a consumption target", async ({ page }) =
4762 await expect ( persisted ) . toBeHidden ( ) ;
4863} ) ;
4964
50- test ( "what-if target stays editable while the factory re-solves" , async ( { page } ) => {
65+ test ( "Scenario target waits for explicit recalculation and reports progress" , async ( { page } ) => {
66+ test . setTimeout ( 120_000 ) ;
5167 await goto ( page , "/factory/scenario" ) ;
5268 await dismissDataDriftPrompt ( page ) ;
69+ await recalculateScenario ( page ) ;
5370
5471 const firstDemand = page . getByRole ( "spinbutton" ) . first ( ) ;
5572 await expect ( firstDemand ) . toBeVisible ( ) ;
73+ const originalTarget = await firstDemand . inputValue ( ) ;
5674 await firstDemand . selectText ( ) ;
5775 await firstDemand . pressSequentially ( "12.34" , { delay : 100 } ) ;
5876
59- // Each character starts another solve. The demand list must remain mounted,
60- // preserving both the in-progress value and keyboard focus throughout them.
61- await expect ( firstDemand ) . toBeFocused ( ) ;
62- await expect ( firstDemand ) . toHaveValue ( "12.34" ) ;
63- await expect ( page . getByText ( "Goal changes" , { exact : false } ) . first ( ) ) . toBeVisible ( {
64- timeout : 15_000 ,
65- } ) ;
77+ // Typing is now a cheap draft: it preserves focus and marks the previous
78+ // result stale instead of launching a whole-factory solve per character.
6679 await expect ( firstDemand ) . toBeFocused ( ) ;
6780 await expect ( firstDemand ) . toHaveValue ( "12.34" ) ;
81+ const status = page . getByTestId ( "scenario-status" ) ;
82+ await expect ( status ) . toHaveAttribute ( "data-state" , "stale" ) ;
83+ await expect ( status ) . toContainText ( "Targets or factory inputs changed" ) ;
84+
85+ await status . getByRole ( "button" , { name : "Recalculate" } ) . click ( ) ;
86+ await expect ( status ) . toHaveAttribute ( "data-state" , "calculating" ) ;
87+ await expect ( status ) . toContainText ( / P r e p a r i n g | r e s p o n s e m o d e l s | S o l v i n g f a c t o r y | V a l i d a t i n g b l o c k s / ) ;
88+ await expect ( status ) . toHaveAttribute ( "data-state" , "current" , { timeout : 90_000 } ) ;
89+ await expect ( status ) . toContainText ( "Scenario is up to date" ) ;
90+
91+ // Restore the shared scratch project's target and its cached result for the
92+ // remaining tests in this serial mutating suite.
93+ await firstDemand . fill ( originalTarget ) ;
94+ await expect ( status ) . toHaveAttribute ( "data-state" , "stale" ) ;
95+ await status . getByRole ( "button" , { name : "Recalculate" } ) . click ( ) ;
96+ await expect ( status ) . toHaveAttribute ( "data-state" , "current" , { timeout : 90_000 } ) ;
97+ } ) ;
98+
99+ test ( "revisiting Scenario reuses the current persisted result" , async ( { page } ) => {
100+ test . setTimeout ( 120_000 ) ;
101+ await goto ( page , "/factory/scenario" ) ;
102+ await dismissDataDriftPrompt ( page ) ;
103+ await recalculateScenario ( page ) ;
104+ const status = page . getByTestId ( "scenario-status" ) ;
105+ const calculatedAt = await status . getAttribute ( "data-calculated-at" ) ;
106+ expect ( calculatedAt ) . toBeTruthy ( ) ;
107+
108+ await page . getByRole ( "link" , { name : "Connections" , exact : true } ) . click ( ) ;
109+ await expect ( page ) . toHaveURL ( / \/ f a c t o r y \/ c o n n e c t i o n s $ / ) ;
110+ await page . getByRole ( "link" , { name : "Scenario" , exact : true } ) . click ( ) ;
111+ await expect ( page ) . toHaveURL ( / \/ f a c t o r y \/ s c e n a r i o $ / ) ;
112+ await expect ( status ) . toHaveAttribute ( "data-state" , "current" ) ;
113+ await expect ( status ) . toHaveAttribute ( "data-calculated-at" , calculatedAt ! ) ;
68114} ) ;
69115
70116/**
@@ -84,6 +130,7 @@ test("applying a scenario re-balances the factory in one undoable step", async (
84130 test . setTimeout ( 120_000 ) ;
85131 await goto ( page , "/factory/scenario" ) ;
86132 await dismissDataDriftPrompt ( page ) ;
133+ await recalculateScenario ( page ) ;
87134
88135 // the Factory pins card is the first place with numeric (spinbutton) inputs
89136 const firstDemand = page . getByRole ( "spinbutton" ) . first ( ) ;
@@ -92,6 +139,11 @@ test("applying a scenario re-balances the factory in one undoable step", async (
92139 // 2× + 1 guarantees a real increase even if the current target reads 0
93140 await firstDemand . fill ( String ( current * 2 + 1 ) ) ;
94141
142+ await page . getByTestId ( "scenario-status" ) . getByRole ( "button" , { name : "Recalculate" } ) . click ( ) ;
143+ await expect ( page . getByTestId ( "scenario-status" ) ) . toHaveAttribute ( "data-state" , "current" , {
144+ timeout : 90_000 ,
145+ } ) ;
146+
95147 // the doubled demand cascades into real per-block changes → Apply scenario enables
96148 const applyAll = page . getByRole ( "button" , { name : "Apply scenario" } ) ;
97149 await expect ( applyAll ) . toBeEnabled ( { timeout : 15_000 } ) ;
@@ -171,6 +223,7 @@ test("Scenario zeros an unpinned consume goal without saving the preview", async
171223
172224 try {
173225 await goto ( page , "/factory/scenario" ) ;
226+ await recalculateScenario ( page ) ;
174227 const change = page . getByRole ( "link" , { name : / ^ A c e t a l d e h y d e / } ) ;
175228 await expect ( change ) . toContainText ( "-2" ) ;
176229 await expect ( change ) . toContainText ( "Next goal/s0" ) ;
@@ -223,6 +276,7 @@ test("Scenario scales a goal beyond recovered coproduct supply", async ({ page }
223276 try {
224277 await goto ( page , "/factory/scenario" ) ;
225278 await dismissDataDriftPrompt ( page ) ;
279+ await recalculateScenario ( page ) ;
226280
227281 await expect ( page . getByRole ( "link" , { name : / ^ C r e o s o t e / } ) ) . toBeVisible ( {
228282 timeout : 60_000 ,
@@ -264,6 +318,7 @@ test("Scenario explains which proposed block goals failed validation", async ({
264318 try {
265319 await goto ( page , "/factory/scenario" ) ;
266320 await dismissDataDriftPrompt ( page ) ;
321+ await recalculateScenario ( page ) ;
267322
268323 const diagnostic = page . getByTestId ( "scenario-validation" ) ;
269324 await expect ( diagnostic . getByText ( "Scenario validation failed" ) ) . toBeVisible ( {
0 commit comments