Skip to content

Commit fbce283

Browse files
thomasahleclaude
andcommitted
Remove waveform meta key; show waveform tab unconditionally
- Drop dead `waveform` field from meta.js (was never read by app code) - Remove `hasVcdSignalDefinitions` guard so the Waves tab appears for any run that produces a VCD, including modules with no traced signals - Update CLAUDE.md to drop the now-removed waveform field docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent aaeb160 commit fbce283

15 files changed

Lines changed: 282 additions & 88 deletions

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ Lessons are defined in a `parts → chapters → lessons` hierarchy and exported
2929
- `files.a`: starter files (keyed by virtual path like `/src/top.sv`)
3030
- `files.b`: solution delta (merged onto `a` to produce the solution)
3131
- `focus`: the default file to show in the editor
32-
- `waveform`: `'off'` | `'optional'` | `'required'` — controls waveform pane visibility
3332
- `html`: inline HTML string for the lesson description
3433

3534
### App State (`src/App.svelte`)

e2e/debug-cocotb.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ test('cocotb: debug - check wasmTable', async ({ page }) => {
1010
await page.goto('/');
1111
await page.getByRole('button', { name: 'cocotb Basics' }).click();
1212
await page.getByRole('button', { name: /Your First cocotb Test/ }).click();
13+
await page.getByTestId('options-button').click();
1314
await page.getByTestId('solve-button').click();
1415
await page.getByTestId('run-button').click();
1516

e2e/formal.spec.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ async function goToLesson(page, chapterName, lessonName) {
1414
await expect(page.getByRole('heading', { level: 2, name: lessonName })).toBeVisible();
1515
}
1616

17+
/** Open the gear menu then click the solve/reset button inside it. */
18+
async function clickSolve(page) {
19+
await page.getByTestId('options-button').click();
20+
await clickSolve(page);
21+
}
22+
1723
// ── BMC: Bounded Model Checking ───────────────────────────────────────────────
1824

1925
test('BMC: starter code has no assertion → verify does not prove anything', async ({ page }) => {
@@ -31,7 +37,7 @@ test('BMC: starter code has no assertion → verify does not prove anything', as
3137
test('BMC: solution proves all properties within the bound', async ({ page }) => {
3238
await goToLesson(page, 'Implication & BMC', 'Bounded Model Checking');
3339

34-
await page.getByTestId('solve-button').click();
40+
await clickSolve(page);
3541
await page.getByTestId('verify-button').click();
3642

3743
const logs = page.getByTestId('runtime-logs');
@@ -54,7 +60,7 @@ test('BMC: shows only verify button, no run button', async ({ page }) => {
5460
test('assume property: solution proves property with constraint', async ({ page }) => {
5561
await goToLesson(page, 'Formal Verification', 'assume property');
5662

57-
await page.getByTestId('solve-button').click();
63+
await clickSolve(page);
5864
await page.getByTestId('verify-button').click();
5965

6066
const logs = page.getByTestId('runtime-logs');
@@ -67,7 +73,7 @@ test('LEC: only shows verify (LEC) button, no run button', async ({ page }) => {
6773
await goToLesson(page, 'Formal Verification', 'Logical Equivalence Checking');
6874

6975
await expect(page.getByTestId('verify-button')).toBeVisible();
70-
await expect(page.getByTestId('verify-button')).toHaveText('verify (LEC)');
76+
await expect(page.getByTestId('verify-button')).toHaveAttribute('aria-label', 'Verify (LEC)');
7177
await expect(page.getByTestId('run-button')).toHaveCount(0);
7278
});
7379

@@ -86,7 +92,7 @@ test('LEC: buggy Impl is detected as NOT equivalent', async ({ page }) => {
8692
test('LEC: fixed Impl is proved equivalent to Spec', async ({ page }) => {
8793
await goToLesson(page, 'Formal Verification', 'Logical Equivalence Checking');
8894

89-
await page.getByTestId('solve-button').click();
95+
await clickSolve(page);
9096
await page.getByTestId('verify-button').click();
9197

9298
const logs = page.getByTestId('runtime-logs');

e2e/lessons.spec.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ async function goToLesson(page, chapterName, lessonName) {
1212
await expect(page.getByRole('heading', { level: 2, name: lessonName })).toBeVisible();
1313
}
1414

15+
/** Open the gear menu then click the solve/reset button inside it. */
16+
async function clickSolve(page) {
17+
await page.getByTestId('options-button').click();
18+
await page.getByTestId('solve-button').click();
19+
}
20+
1521
async function expectInterpretMode(logs) {
1622
await expect(logs).toContainText('--mode interpret');
1723
await expect(logs).not.toContainText('--compiled');
@@ -34,14 +40,24 @@ test('solve/reset toggles between solution and starter', async ({ page }) => {
3440
await page.goto('/');
3541
await page.getByRole('button', { name: 'next' }).click();
3642

43+
// Open menu and verify initial state
44+
await page.getByTestId('options-button').click();
3745
const solveBtn = page.getByTestId('solve-button');
38-
await expect(solveBtn).toHaveText('solve');
46+
await expect(solveBtn).toHaveText('Show solution');
3947

48+
// Apply solution
4049
await solveBtn.click();
41-
await expect(solveBtn).toHaveText('reset');
4250

51+
// Reopen menu and verify reset state
52+
await page.getByTestId('options-button').click();
53+
await expect(solveBtn).toHaveText('Reset to starter');
54+
55+
// Reset
4356
await solveBtn.click();
44-
await expect(solveBtn).toHaveText('solve');
57+
58+
// Reopen menu and verify back to solve
59+
await page.getByTestId('options-button').click();
60+
await expect(solveBtn).toHaveText('Show solution');
4561
});
4662

4763
// ── SystemVerilog Basics ──────────────────────────────────────────────────────
@@ -60,7 +76,7 @@ test('Welcome: run outputs Hello World', async ({ page }) => {
6076
test('Up-Counter: solution simulates and produces a waveform', async ({ page }) => {
6177
await goToLesson(page, 'Sequential Logic', 'Up-Counter');
6278

63-
await page.getByTestId('solve-button').click();
79+
await clickSolve(page);
6480
await page.getByTestId('run-button').click();
6581

6682
const logs = page.getByTestId('runtime-logs');
@@ -87,7 +103,7 @@ test('Modules and Ports: waveform renders after solve and run', async ({ page })
87103
await page.getByRole('button', { name: 'next' }).click();
88104
await expect(page.getByTestId('lesson-title')).toHaveText('Modules and Ports');
89105

90-
await page.getByTestId('solve-button').click();
106+
await clickSolve(page);
91107
await page.getByTestId('run-button').click();
92108

93109
const logs = page.getByTestId('runtime-logs');
@@ -105,9 +121,9 @@ test('Modules and Ports: waveform renders after solve and run', async ({ page })
105121
// ── SystemVerilog Assertions ──────────────────────────────────────────────────
106122

107123
test('immediate-assert: solution passes assertions', async ({ page }) => {
108-
await goToLesson(page, 'Your First Assertion', 'Immediate Assertions');
124+
await goToLesson(page, 'Your First Formal Assertion', 'Immediate Assertions');
109125

110-
await page.getByTestId('solve-button').click();
126+
await clickSolve(page);
111127
await page.getByTestId('verify-button').click();
112128

113129
const logs = page.getByTestId('runtime-logs');
@@ -116,9 +132,9 @@ test('immediate-assert: solution passes assertions', async ({ page }) => {
116132
});
117133

118134
test('sequence-basics: solution runs without errors', async ({ page }) => {
119-
await goToLesson(page, 'Your First Assertion', 'Sequences and Properties');
135+
await goToLesson(page, 'Your First Formal Assertion', 'Sequences and Properties');
120136

121-
await page.getByTestId('solve-button').click();
137+
await clickSolve(page);
122138
await page.getByTestId('verify-button').click();
123139

124140
const logs = page.getByTestId('runtime-logs');

e2e/live-smoke.spec.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ test('[app] loads and shows lesson navigation', async ({ page }) => {
3535

3636
test('[sim] Welcome — run button produces output, no compile error', async ({ page }) => {
3737
await goToLesson(page, 'Introduction', 'Welcome');
38+
await page.getByTestId('options-button').click();
3839
await page.getByTestId('solve-button').click();
3940
await page.getByTestId('run-button').click();
4041
const logs = page.getByTestId('runtime-logs');
@@ -43,6 +44,7 @@ test('[sim] Welcome — run button produces output, no compile error', async ({
4344

4445
test('[sim] Concurrent Assertions — runtime assertions chapter visible, assertionFail produced', async ({ page }) => {
4546
await goToLesson(page, 'Runtime Assertions', 'Concurrent Assertions in Simulation');
47+
await page.getByTestId('options-button').click();
4648
await page.getByTestId('solve-button').click();
4749
await page.getByTestId('run-button').click();
4850
const logs = page.getByTestId('runtime-logs');
@@ -52,6 +54,7 @@ test('[sim] Concurrent Assertions — runtime assertions chapter visible, assert
5254

5355
test('[bmc] Immediate Assertions — verify runs z3', async ({ page }) => {
5456
await goToLesson(page, 'Your First Formal Assertion', 'Immediate Assertions');
57+
await page.getByTestId('options-button').click();
5558
await page.getByTestId('solve-button').click();
5659
await page.getByTestId('verify-button').click();
5760
const logs = page.getByTestId('runtime-logs');
@@ -62,6 +65,7 @@ test('[bmc] Immediate Assertions — verify runs z3', async ({ page }) => {
6265

6366
test('[bmc] Implication — verify runs z3', async ({ page }) => {
6467
await goToLesson(page, 'Implication & BMC', 'Implication: |-> and |=>');
68+
await page.getByTestId('options-button').click();
6569
await page.getByTestId('solve-button').click();
6670
await page.getByTestId('verify-button').click();
6771
const logs = page.getByTestId('runtime-logs');
@@ -72,6 +76,7 @@ test('[bmc] Implication — verify runs z3', async ({ page }) => {
7276

7377
test('[lec] Logical Equivalence Checking — z3 proves unsat', async ({ page }) => {
7478
await goToLesson(page, 'Formal Verification', 'Logical Equivalence Checking');
79+
await page.getByTestId('options-button').click();
7580
await page.getByTestId('solve-button').click();
7681
await page.getByTestId('verify-button').click();
7782
const logs = page.getByTestId('runtime-logs');

e2e/qa-all-lessons.spec.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,14 @@ for (const [index, lesson] of LESSONS.entries()) {
7979
await expect(page.getByTestId('lesson-title')).toHaveText(lesson.title, { timeout: 10_000 });
8080

8181
// Apply the solution
82+
await page.getByTestId('options-button').click();
8283
const solveBtn = page.getByTestId('solve-button');
8384
if (await solveBtn.count() > 0) {
84-
await solveBtn.click();
85-
await expect(solveBtn).toHaveText('reset', { timeout: 5_000 });
85+
const label = ((await solveBtn.textContent()) ?? '').trim();
86+
if (label !== 'Reset to starter') await solveBtn.click();
87+
else await page.keyboard.press('Escape');
88+
} else {
89+
await page.keyboard.press('Escape');
8690
}
8791

8892
const logs = page.getByTestId('runtime-logs');

e2e/solutions-live.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ for (const lesson of LESSONS) {
5454
test(`[${lesson.runner ?? 'sim'}] ${lesson.title}`, async ({ page }) => {
5555
await goToLesson(page, lesson.chapter, lesson.title);
5656

57-
const solveBtn = page.getByTestId('solve-button');
58-
await solveBtn.click();
57+
await page.getByTestId('options-button').click();
58+
await page.getByTestId('solve-button').click();
5959

6060
const logs = page.getByTestId('runtime-logs');
6161

e2e/solutions.spec.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,9 @@ for (const lesson of LESSONS) {
154154
test(`[${lesson.runner ?? 'sim'}] ${lesson.title}`, async ({ page }) => {
155155
await goToLesson(page, lesson.chapter, lesson.title);
156156

157-
const solveBtn = page.getByTestId('solve-button');
158-
// If already solved (button shows "reset"), re-apply by clicking reset then solve.
159-
// More simply: clicking solve always sets the workspace to the solution.
160-
await solveBtn.click();
157+
// Open the gear menu then apply the solution.
158+
await page.getByTestId('options-button').click();
159+
await page.getByTestId('solve-button').click();
161160

162161
const logs = page.getByTestId('runtime-logs');
163162

e2e/uvm.spec.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,17 @@ async function goToLesson(page, lesson) {
2626
}
2727

2828
async function applySolution(page) {
29+
await page.getByTestId('options-button').click();
2930
const solveBtn = page.getByTestId('solve-button');
30-
if (await solveBtn.count() === 0) return;
31+
if (await solveBtn.count() === 0) { await page.keyboard.press('Escape'); return; }
3132

32-
const label = ((await solveBtn.textContent()) ?? '').trim().toLowerCase();
33-
if (label === 'reset') {
34-
await solveBtn.click();
35-
await expect(solveBtn).toHaveText(/solve/i);
33+
const label = ((await solveBtn.textContent()) ?? '').trim();
34+
if (label === 'Reset to starter') {
35+
// Already solved — close menu and return
36+
await page.keyboard.press('Escape');
37+
return;
3638
}
3739
await solveBtn.click();
38-
await expect(solveBtn).toHaveText(/reset/i);
3940
}
4041

4142
async function expectCleanUvmRun(logs) {

e2e/waveform.spec.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,89 @@ test('waveform toolbar buttons send correct commands', async ({ page }) => {
139139
}
140140
});
141141

142+
test('transition_next selects earliest-transitioning signal, not alphabetically first', async ({ page }) => {
143+
// Surfer displays signals alphabetically. For priority-enc the order is:
144+
// grant (idx 0), req (idx 1), valid (idx 2)
145+
// But req and valid both transition at t=1 (the first simulation step), while
146+
// grant only changes at t=2. So the auto-selected signal should be req or valid,
147+
// NOT grant — otherwise transition_next from t=0 skips to a later timestamp.
148+
await page.goto('/surfer/index.html#dev');
149+
const crashBanner = page.getByText('Sorry, Surfer crashed');
150+
let surferBootCrash = false;
151+
try {
152+
await expect(crashBanner).toBeVisible({ timeout: 3000 });
153+
surferBootCrash = true;
154+
} catch {
155+
surferBootCrash = false;
156+
}
157+
test.skip(surferBootCrash, 'Surfer crashes in this Playwright environment (WebGL unavailable)');
158+
159+
await page.goto('/lesson/sv/priority-enc');
160+
await page.evaluate(() => {
161+
for (const key of Object.keys(localStorage)) {
162+
if (key.startsWith('svt:')) localStorage.removeItem(key);
163+
}
164+
});
165+
166+
await page.getByTitle('Editor options').click();
167+
await page.getByTestId('solve-button').click();
168+
await page.getByTestId('run-button').click();
169+
170+
await expect(page.getByTestId('runtime-tab-waves')).toBeVisible({ timeout: 90_000 });
171+
await page.getByTestId('runtime-tab-waves').click();
172+
173+
const waveFrame = page.getByTestId('waveform-frame-wrapper');
174+
await expect(waveFrame).toHaveAttribute('data-wave-state', 'ready', { timeout: 30_000 });
175+
176+
// data-selected-signal is set when SetItemSelected succeeds in the polling loop.
177+
// If it stays empty, id_of_name never returned a valid item (signals weren't ready).
178+
// If it equals 'grant', the wrong signal was selected (alphabetically first ≠ earliest).
179+
await expect.poll(
180+
() => waveFrame.getAttribute('data-selected-signal'),
181+
{ timeout: 10_000, message: 'data-selected-signal should be set to a transitioning signal (not grant or empty)' }
182+
).toMatch(/^(req|valid)$/);
183+
});
184+
185+
test('open-in-surfer button opens Surfer popup and sends VCD', async ({ page }) => {
186+
// Intercept blob creation on the main page so we can verify the VCD blob is
187+
// prepared for the popup (openInNewWindow creates a fresh blob URL from vcd prop).
188+
await page.addInitScript(() => {
189+
window._blobTexts = [];
190+
const orig = URL.createObjectURL.bind(URL);
191+
URL.createObjectURL = function (blob) {
192+
const url = orig(blob);
193+
if (blob.type === 'text/plain') {
194+
blob.text().then((t) => window._blobTexts.push(t));
195+
}
196+
return url;
197+
};
198+
});
199+
200+
await runModulesAndPorts(page);
201+
await page.getByTestId('runtime-tab-waves').click();
202+
203+
const waveFrame = page.getByTestId('waveform-frame-wrapper');
204+
await expect(waveFrame).toHaveAttribute('data-wave-state', 'ready', { timeout: 30_000 });
205+
206+
const openBtn = page.getByTitle('Open in Surfer');
207+
await expect(openBtn).toBeEnabled();
208+
209+
// window.open() fires the 'popup' event on the Playwright context.
210+
const popupPromise = page.waitForEvent('popup');
211+
await openBtn.click();
212+
const popup = await popupPromise;
213+
214+
// The popup should navigate to the Surfer index page.
215+
await popup.waitForURL(/surfer\/index\.html/, { timeout: 10_000 });
216+
217+
// A VCD blob should have been created for the popup.
218+
// VCD files start with "$date" or "$timescale" while scope command files start with "scope_".
219+
await expect.poll(
220+
() => page.evaluate(() => window._blobTexts.some((t) => t.includes('$var'))),
221+
{ timeout: 5_000, message: 'Expected a VCD blob to be created for the popup' }
222+
).toBe(true);
223+
});
224+
142225
test('concurrent-sim SVA assertion signal appears in VCD', async ({ page }) => {
143226
// Intercept URL.createObjectURL to capture the text of any VCD blob sent to Surfer.
144227
await page.addInitScript(() => {

0 commit comments

Comments
 (0)