diff --git a/frontend/src/__tests__/simulation-parts.test.ts b/frontend/src/__tests__/simulation-parts.test.ts index 15d9edc8..45aa3341 100644 --- a/frontend/src/__tests__/simulation-parts.test.ts +++ b/frontend/src/__tests__/simulation-parts.test.ts @@ -557,6 +557,50 @@ describe('7segment — attachEvents', () => { expect(unsubMock).toHaveBeenCalledTimes(8); }); + it('re-attach after digits changes 1→4 subscribes DIG pins (agent build order)', () => { + // The agent adds the display with default digits=1, THEN sets digits=4, + // then wires, compiles (hexEpoch bump → re-attach) and runs. The cached + // per-element state must rebuild for the new digit count, or the + // re-attach keeps subscribing COM.1/COM.2 and the display stays blank + // until a page reload. + const logic = PartSimulationRegistry.get('7segment')!; + const el = makeElement({ values: new Array(8).fill(0), digits: 1 }); + const sim = makeSimulator(); + const subscribed: number[] = []; + sim.pinManager.onPinChange.mockImplementation((pin: number) => { + subscribed.push(pin); + return () => {}; + }); + + // First attach while digits=1 (mid-build): subscribes COM pins only. + const pins1 = pinMap({ 'COM.1': 30, 'COM.2': 31, DIG1: 10, DIG2: 11, DIG3: 12, DIG4: 13, A: 2 }); + const cleanup1 = logic.attachEvents!(el, sim as any, pins1); + expect(subscribed).toContain(30); // COM.1 (single-digit mode) + expect(subscribed).not.toContain(10); // DIG1 not subscribed yet + cleanup1(); + + // Property set later: digits=4. Re-attach (compile/run does this). + (el as any).digits = 4; + subscribed.length = 0; + const callbacks = new Map void>(); + sim.pinManager.onPinChange.mockImplementation( + (pin: number, cb: (pin: number, state: boolean) => void) => { + subscribed.push(pin); + callbacks.set(pin, cb); + return () => {}; + }, + ); + logic.attachEvents!(el, sim as any, pins1); + expect(subscribed).toEqual(expect.arrayContaining([10, 11, 12, 13])); // DIG1..4 + + // And the display actually lights: segment A high + DIG1 enabled. + callbacks.get(2)!(2, true); // segment A high + callbacks.get(10)!(10, true); // DIG1 on → latches segments + const values = (el as any).values as number[]; + expect(values.length).toBe(32); // 4 digits × 8 segments + expect(values[0]).toBe(1); // digit 0, segment A lit + }); + it('CLN pin drives colon + colonValue (attachEvents and onPinStateChange)', () => { const logic = PartSimulationRegistry.get('7segment')!; diff --git a/frontend/src/simulation/parts/ChipParts.ts b/frontend/src/simulation/parts/ChipParts.ts index 42ec58a4..639000b9 100644 --- a/frontend/src/simulation/parts/ChipParts.ts +++ b/frontend/src/simulation/parts/ChipParts.ts @@ -94,8 +94,20 @@ function getDigitsCount(element: HTMLElement): number { function get7SegState(element: HTMLElement): SevenSegState { let s = sevenSegState.get(element); + const digits = getDigitsCount(element); + // Rebuild when the digit count CHANGED since the state was created. The + // agent adds the display with the default digits=1 and only then sets + // digits=4 — the stale 1-digit state made every later attachEvents + // subscribe COM.1/COM.2 (which don't exist on a 4-digit part) instead of + // DIG1..DIG4, and since those resolvers "attached", the all-digits-on + // fallback never kicked in either: the display stayed permanently blank + // in-session and only a page reload (fresh element, digits already 4) + // fixed it. + if (s && s.digits !== digits) { + if (s.flushTimer !== null) clearTimeout(s.flushTimer); + s = undefined; + } if (!s) { - const digits = getDigitsCount(element); s = { digits, segments: [0, 0, 0, 0, 0, 0, 0, 0],