Circuit-Editor and Visualization Re-Architecting and Tests#3425
Circuit-Editor and Visualization Re-Architecting and Tests#3425ScottCarda-MS wants to merge 25 commits into
Conversation
…g references to planning docs
amcasey
left a comment
There was a problem hiding this comment.
As discussed, I focused my review on the tests, since I understand the rest is basically just shuffling code around.
I was a little surprised not to see more validation of our handling of bad input, given that users and copilot can directly edit qsc files.
| }); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // removeOperation |
There was a problem hiding this comment.
I may have overlooked tests for the following:
- Removing the only (remaining) gate
- Removing a gate at an invalid location
There was a problem hiding this comment.
| const unitary = (/** @type {string} */ g) => gate(g, 0); | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // addOperation |
There was a problem hiding this comment.
What happens if I add an operation at 0,1000 or 1000,0?
There was a problem hiding this comment.
They will return null. Look at these tests:
| removeOperation(model, "0,0-0,1"); | ||
|
|
||
| // Outer group remains; the X inside its single child column is gone. | ||
| expectOp(at(model, "0,0"), { Group: { children: [[{ H: 0 }]] } }); |
There was a problem hiding this comment.
What happens if you remove the H too?
There was a problem hiding this comment.
It should prune any empty groups, but I realize I only tested that for the move-actions, and not for the pure delete-action. Updated!
removeOperation: removing a group's last child prunes the emptied group
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| // Remove-mutator tests on grouped shapes, driven through the public |
There was a problem hiding this comment.
The file name seems to imply that there will also be add tests?
There was a problem hiding this comment.
Some existing tests were spread across other files. I've moved them here to consolidate and also added some tests in-line with the addRemove test file.
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| // Ancestor `.targets` cache refresh after a child mutation: when |
There was a problem hiding this comment.
I had trouble following these tests (just my own ignorance) but I'm wondering if the following are covered (here or elsewhere):
- Moving a gate into a parent group
- Moving a gate into a grandparent group
- Moving a gate into a child group
- Moving a gate into a sibling group
Or possibly we don't allow any of those changes.
There was a problem hiding this comment.
- Moving a gate "into" its parent group is just moving the gate inside the group it already is in. Here is an example test: moveOperation: moving an internal gate to its group's trailing inner-column slot keeps it inside the group
- Good idea, added a new test: moveOperation: promoting a gate into its grandparent group lands it beside the parent group
- This doesn't make sense to me. We don't currently support moving groups (look to a future PR for that), but even when we do, you are asking about a group moving into itself. I'm not sure how we'd express that in the current API. If you have more thoughts on this, let's follow up on this offline.
- Good idea, added a new test: moveOperation: moving a gate into a sibling group relocates it across scopes
|
|
||
| const prompt = getOpenPrompt(); | ||
| assert.ok(prompt); | ||
| prompt.okButton.click(); |
There was a problem hiding this comment.
Is there coverage of what happens when cancel is clicked?
There was a problem hiding this comment.
There's a test above this one that checks for cancel behavior for this scenario, and cancel is generally tested throughout the tests here. Maybe I'm missing what you are asking about?
| assert.equal(el().scrollTop, 10); | ||
| }); | ||
|
|
||
| test("mousemove near the left edge scrolls left", () => { |
There was a problem hiding this comment.
Do we have some reason to believe the left and right edges might have different scrolling behavior? Some of these tests seem redundant.
There was a problem hiding this comment.
Yes, the left edge is special and has special behavior because we expect users to drag gates from the toolbox from the left side onto the circuit. The special behavior is that we disable scrolling at first from the left when dragging from the toolbar, but once the drag gesture has gotten significantly past the left edge, then it becomes enabled again and the user can scroll along the left edge like a normal edge.
| const toolbox = createToolboxElement(() => {}); | ||
|
|
||
| const buttons = toolbox.querySelectorAll(".svg-run-button"); | ||
| assert.equal(buttons.length, 1, "exactly one Run button expected"); |
There was a problem hiding this comment.
Is there product code that could generate more than one button and then simplifies away the redundant ones? Why are we concerned that there might be too many Run buttons?
There was a problem hiding this comment.
It is really just asserting that there is a button in this scenario, because the previous test covers the scenario where there is no button.
| - `angleExpression.ts` — parse/normalize/evaluate gate rotation | ||
| angle expressions (handles `pi`/`π` and simple arithmetic). | ||
|
|
||
| The hard rule that drives the layering: **`data/` and `actions/` |
There was a problem hiding this comment.
Is there a way we can enforce that they don't touch the dom?
There was a problem hiding this comment.
Since the tests that run off of these layers don't have a DOM, if someone adds a querySelector call, for example, to the logic of these layers, the tests will complain about not having a document to query. Sort of an indirect way to enforce it though.
I could add lints to enforce this.
There was a problem hiding this comment.
I looked into adding lints. It is doable, but I kind of thing it is overkill for this.
|
Here are some findings from AI-assisted review.
For 4-5, I asked Copilot to find what this PR does beyond pure refactoring. Ideally, I'd suggest having a PR doing pure refactoring, and then add new features in a follow-up PR(s), to minimize risk of bugs in the large refactoring PR. I am not asking to do the split, just review these items and make sure these do not introduce bugs. For 6, I asked Copilot to suggest what tests could be added, so feel free to ignore it. |
There was a problem hiding this comment.
Please include in this doc explanation why it's called "Sqore".
is this an abbreviation or pun on "score"? If I don't know the origin of a term, I will be wondering about it every time I see word "sqore" which is distracting.
There was a problem hiding this comment.
I think it is better to just explain what I can here instead of clutter the doc with historical context. "Sqore" was the original name of the circuit visualization library when we inherited it. This used to be a separate tool developed by another team under our org. I think it is a reference to musical scores but used a "q" because that's common in quantum to insert "q" into the name of things.
I'm not opposed to changing the name, but nothing specific comes to mind, and I thought it was harmless for now to keep it the original name.
Circuit Editor Re-Architecture: Layered Foundation (Data → Actions → Renderer → Editor)
At a glance
source/npm/qsharp/ux/circuit-vis/**+source/npm/qsharp/test/**only.mjstests. No Rust, no VS Code host changes.index.tsexports the same surfaceWhat this PR does
Splits the monoliths into a layered architecture. The old editor was
four large files with tangled responsibilities. They are removed and
replaced with a strict, one-directional layering:
The hard rule:
data/andactions/never touch the DOM. The rendererreads data to produce SVG; the editor glues DOM events to action calls.
Adds group-aware editing foundations. Editing inside expanded groups,
the group structure and add/remove/ancestor-refresh/collision-split
behaviors, single-leg move and control-drag paths, and the dropzone geometry
the editor is built on.
Hardens classical-register / measurement integrity. Moving or deleting a
measurement that later gates depend on now cascades correctly (with a confirm
prompt) instead of crashing or leaving dangling classical references.
Persists view state. Group expand/collapse and related view state survive
re-render via the
ViewStatetype threaded throughsqore.ts.Adds an in-tree architecture guide. A new
ux/circuit-vis/ARCHITECTURE.mddocuments the layering, the render/click flows, and the module invariants —
useful for this review and as durable documentation afterward.
Files removed (old monoliths)
circuitManipulation.tsevents.tspanel.tsdraggable.ts(old version)contextMenu.ts(old version)Their responsibilities now live in the layered modules.
How to review this efficiently
This PR adds a structural walkthrough alongside the code:
ux/circuit-vis/ARCHITECTURE.md.Read its TL;DR and module map first — it explains the layering, has diagrams of
a render and a click flow, and lists the invariants. Then follow the order
below.
Suggested order and natural stopping points:
data/) — small, pure value types (Location,CircuitModel,ViewState). The vocabulary everything else is written in.actions/+actions/circuit-actions/) — pure mutations;the correctness core. Cross-reference the
test/circuit-editor/circuit-actions/suite as you go.→ Good checkpoint: you've now seen the entire pure core.
renderer/) —Circuit→ SVG; reads data, never mutates.editor/+editor/controllers/) — DOM glue. Read the sharedInteractionContextfirst, then the controllers.→ Good checkpoint: trace one end-to-end flow from ARCHITECTURE.md.
sqore.ts,utils.ts,index.ts) — ties it together.state-viz/) — parallel subsystem, only import-path updateshere; safe to skim last.
What to focus your scrutiny on
You don't need prior familiarity with this code to review it well. If you only
have time for a few things, these are the questions worth asking — each one is
answerable by reading a file or two:
what the editor does. The tests are the contract: if the suite is green and
the snapshot fixtures are unchanged, existing behavior is preserved. Skim the
test names in
test/circuit-editor/to see what's covered.one-way dependency: the pure logic (
data/,actions/) computes circuitchanges, and only the
editor/layer touches the browser/DOM. Spot-check acouple of
data/andactions/files — they should have no DOM orbrowser-API calls. If they don't, the separation holds.
the render path in
sqore.tsworks on a copy, so the caller's circuit objectis left untouched.
the audience the new layout is meant to help. If a folder or file name doesn't
match what's inside, that's useful feedback.
Everything else (cache-consistency details, controller wiring, drag cleanup) is
covered by the test suite.
What you can safely skim
state-viz/diffs — mechanical import-path updates from the move.test/circuits-cases/— generated.Testing
392 circuit-editor tests across 28
.mjsfiles, all passing (data,actions/circuit-actions,renderer, controllers,sqore), plus snapshotfixtures under
test/circuits-cases/.The action layer is the most heavily covered: the
test/circuit-editor/circuit-actions/suite pins the single-leg move /control / classical-ref-remap / group-structure paths.
Run locally from
source/npm/qsharp/: