Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c2cd30d
circuit-editor foundation re-arch
ScottCarda-MS Jul 7, 2026
28ff077
Added the architecture markdown file
ScottCarda-MS Jul 8, 2026
384c38d
Merge branch 'main' into sccarda/ce-foundation
ScottCarda-MS Jul 8, 2026
8568700
make the mermaid diagrams more readable
ScottCarda-MS Jul 9, 2026
f0da218
add some missing content to the Architecture file
ScottCarda-MS Jul 9, 2026
a086285
decided the benchmarking should be excluded, cleaned up some remainin…
ScottCarda-MS Jul 9, 2026
9d26c36
consolidate data model tests
ScottCarda-MS Jul 14, 2026
099257a
consolidated/removed some tests in the action/view layers
ScottCarda-MS Jul 14, 2026
e346b0e
dropped a couple of view layer tests
ScottCarda-MS Jul 14, 2026
5af8dea
ARCHITECTURE file to replace old README
ScottCarda-MS Jul 15, 2026
2bcd93d
Don't wrap comments below 100 cols
ScottCarda-MS Jul 15, 2026
5739711
comments don't wrap under 100 cols in tests
ScottCarda-MS Jul 15, 2026
22d93c4
condensed comment
ScottCarda-MS Jul 15, 2026
5d7db8a
added some addRemove tests
ScottCarda-MS Jul 16, 2026
f41ddcf
added some movement tests
ScottCarda-MS Jul 16, 2026
2cc4e21
reworded
ScottCarda-MS Jul 16, 2026
f8cae3d
remove empty out groups
ScottCarda-MS Jul 16, 2026
f535adb
remove unused import
ScottCarda-MS Jul 16, 2026
935bc2e
Move/added some group-add tests
ScottCarda-MS Jul 16, 2026
d4c2059
add test for moving a doomed circuit wire
ScottCarda-MS Jul 16, 2026
57c6560
more instructions about the geometry constants in the test file
ScottCarda-MS Jul 16, 2026
d8dd8b6
Remove local parseLocation in test.
ScottCarda-MS Jul 16, 2026
10e8bea
small rename
ScottCarda-MS Jul 17, 2026
ca557f9
trim some tests
ScottCarda-MS Jul 17, 2026
91a3f56
reflow README
ScottCarda-MS Jul 17, 2026
012db0a
stop view state leak for non-editable circuits
ScottCarda-MS Jul 17, 2026
37017d2
better separation of utils functions along DOM/no-DOM boundary
ScottCarda-MS Jul 17, 2026
36e0331
Test to ensure view state gets removed when a new circuit is made.
ScottCarda-MS Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
541 changes: 541 additions & 0 deletions source/npm/qsharp/test/circuit-editor/_helpers.mjs

Large diffs are not rendered by default.

218 changes: 218 additions & 0 deletions source/npm/qsharp/test/circuit-editor/angleExpression.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

// angleExpression tests — direct coverage for the two helpers in
// `angleExpression.ts` that drive the Edit Argument input prompt
// in [contextMenu.ts](../../ux/circuit-vis/editor/contextMenu.ts):
//
// - `isValidAngleExpression(expr)` is the predicate the prompt
// consults on every keystroke to enable/disable OK. Anything it
// accepts ends up persisted into the operation's `args`.
// - `normalizeAngleExpression(expr)` runs BEFORE validation: it
// trims surrounding whitespace and folds case-insensitive `pi`
// to `π`. The persisted value is the normalized form, so OK's
// enabled state must agree with what the user sees after Save.

// @ts-check

import { test } from "node:test";
import assert from "node:assert/strict";
import {
isValidAngleExpression,
normalizeAngleExpression,
} from "../../dist/ux/circuit-vis/angleExpression.js";

// ---------------------------------------------------------------------------
// isValidAngleExpression — positive cases
// ---------------------------------------------------------------------------

test("isValidAngleExpression: plain numbers (positive, negative, decimal) are valid", () => {
// The simplest path — the prompt should accept any literal
// numeric value the user types, including the "5." trailing-dot
// form (mirroring JavaScript's `parseFloat` tolerance).
assert.equal(isValidAngleExpression("0"), true);
assert.equal(isValidAngleExpression("5"), true);
assert.equal(isValidAngleExpression("-5"), true);
assert.equal(isValidAngleExpression("+5"), true);
assert.equal(isValidAngleExpression("3.5"), true);
assert.equal(isValidAngleExpression("-3.5"), true);
assert.equal(isValidAngleExpression("5."), true);
});

test("isValidAngleExpression: bare π is valid in all four case forms", () => {
// The prompt's placeholder example is `"π / 2.0"`; the user
// can type π directly via the on-screen π button, or use any
// case variant of `pi` which `normalizeAngleExpression` folds
// to π before this check runs.
assert.equal(isValidAngleExpression("π"), true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to assert that these are not only valid but equivalent?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And what about Π? 😆

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The angleExpression isn't evaluated here. Really, no part of the circuit is evaluated like that in the editor or visualizer. That's the job of the generated Q# code, which is very much separate from the circuit or editor code. That's why these tests very specifically focus only on the validation of the syntax. The actual expression is carried forward to the Q# and evaluated by the Q# runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capital Π is not supported because we don't expect users to be able to type that in. Lower-case is only supported because that's what other representations get converted into.

assert.equal(isValidAngleExpression("pi"), true);
assert.equal(isValidAngleExpression("Pi"), true);
assert.equal(isValidAngleExpression("PI"), true);
});

test("isValidAngleExpression: signed π is valid", () => {
// The `-π` and `+π` forms are the unary-sign-on-pi-factor path
// through the parser; pin both.
assert.equal(isValidAngleExpression("-π"), true);
assert.equal(isValidAngleExpression("+π"), true);
assert.equal(isValidAngleExpression("-pi"), true);
});

test("isValidAngleExpression: arithmetic combinations are valid", () => {
// The four supported binary operators, with both numbers and π.
// These mirror the prompt's example text ("π / 2.0", "2.0 * π").
assert.equal(isValidAngleExpression("2 + 3"), true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would you need addition?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed silly to support multiplication and division (which is useful in most expression with pi) without also supporting addition/subtraction and parens. Being able to support basic arithmetic will be useful when we add support for referencing things other than numbers here, but that is an idea for the future.

assert.equal(isValidAngleExpression("2 - 3"), true);
assert.equal(isValidAngleExpression("2 * 3"), true);
assert.equal(isValidAngleExpression("6 / 2"), true);
assert.equal(isValidAngleExpression("π / 2"), true);
assert.equal(isValidAngleExpression("2 * π"), true);
assert.equal(isValidAngleExpression("2.0 * π"), true);
});

test("isValidAngleExpression: parentheses (including nesting) are valid", () => {
// Recursive descent through `parseFactor`'s `lpar` branch.
assert.equal(isValidAngleExpression("(π)"), true);
assert.equal(isValidAngleExpression("(2 + 3) * π"), true);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would find it reassuring to know that these expressions had been evaluated correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in, you want a test that putting in "(3 + 4) * 7" yields 49?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The evaluation of the arithmetic is not performed by the editor, as that is a Q# runtime task.

assert.equal(isValidAngleExpression("((π))"), true);
assert.equal(isValidAngleExpression("π / (2 * 3)"), true);
});

test("isValidAngleExpression: leading/trailing whitespace is tolerated", () => {
// `normalizeAngleExpression` (called internally by the
// evaluator) trims; the prompt also normalizes the value
// BEFORE this predicate runs, so being lenient here matches
// what the user sees after the auto-trim.
assert.equal(isValidAngleExpression(" π "), true);
assert.equal(isValidAngleExpression("\tπ / 2\n"), true);
});

// ---------------------------------------------------------------------------
// isValidAngleExpression — negative cases
// ---------------------------------------------------------------------------

test("isValidAngleExpression: empty / whitespace-only input is invalid", () => {
// The prompt's default OK-disabled state for an empty input.
// `evaluateAngleExpression` short-circuits on a falsy `expr`
// or a falsy post-normalize string.
assert.equal(isValidAngleExpression(""), false);
assert.equal(isValidAngleExpression(" "), false);
assert.equal(isValidAngleExpression("\t\n"), false);
});

test("isValidAngleExpression: unknown characters are invalid", () => {
// Any character outside `[0-9.+\-*/()\sπ]` (after the `pi` →
// `π` fold) takes the tokenizer's "unknown character" fallthrough
// and returns undefined. Pin a few common typos.
assert.equal(isValidAngleExpression("π^2"), false);
assert.equal(isValidAngleExpression("sin(π)"), false);
assert.equal(isValidAngleExpression("π & 2"), false);
assert.equal(isValidAngleExpression("π,2"), false);
});

test("isValidAngleExpression: malformed numbers are invalid", () => {
// The number tokenizer requires `\d+(\.\d*)?` — no leading dot,
// no multiple decimals. Both are rejected.
assert.equal(
isValidAngleExpression(".5"),
false,
"leading dot is not a valid number",
);
assert.equal(
isValidAngleExpression("1.2.3"),
false,
"multiple decimal points are not a valid number",
);
});

test("isValidAngleExpression: unbalanced parentheses are invalid", () => {
// `parseFactor`'s `lpar` branch requires a matching `rpar`
// and returns undefined if it doesn't see one. The "extra
// rpar" case fails the trailing `k !== toks.length` guard.
assert.equal(isValidAngleExpression("(π"), false);
assert.equal(isValidAngleExpression("π)"), false);
assert.equal(isValidAngleExpression("((π)"), false);
});

test("isValidAngleExpression: trailing / dangling operators are invalid", () => {
// `parseExpr` / `parseTerm` call `parseFactor` after consuming
// an operator; if the RHS factor is missing, the parse returns
// undefined.
assert.equal(isValidAngleExpression("π +"), false);
assert.equal(isValidAngleExpression("π *"), false);
assert.equal(isValidAngleExpression("2 +"), false);
});

test("isValidAngleExpression: lone operators / empty parens are invalid", () => {
// No factor at all (operator only, empty parens) → parseFactor
// returns undefined on the first call.
assert.equal(isValidAngleExpression("+"), false);
assert.equal(isValidAngleExpression("-"), false);
assert.equal(isValidAngleExpression("*"), false);
assert.equal(isValidAngleExpression("()"), false);
});

test("isValidAngleExpression: infinite results (division by zero) are invalid", () => {
// The evaluator's final `!isFinite(result)` guard rejects
// `1/0` and friends — the prompt should not allow the user
// to commit an angle that would evaluate to ±Infinity.
assert.equal(isValidAngleExpression("1 / 0"), false);
assert.equal(isValidAngleExpression("π / 0"), false);
assert.equal(isValidAngleExpression("-1 / 0"), false);
});

test("isValidAngleExpression: adjacent factors without an operator are invalid", () => {
// Implicit multiplication isn't supported; "2π" is rejected
// even though it's a common math-notation shorthand. The
// parser leaves the `pi` token unconsumed after `parseFactor`
// returns `2`, then the trailing `k !== toks.length` guard
// trips. (If implicit-multiply support is added later, this
// test should be updated to expect `true`.)
assert.equal(isValidAngleExpression("2π"), false);
assert.equal(isValidAngleExpression("π2"), false);
});

// ---------------------------------------------------------------------------
// normalizeAngleExpression
// ---------------------------------------------------------------------------

test("normalizeAngleExpression: trims surrounding whitespace", () => {
// The prompt persists the normalized value, so leading/trailing
// whitespace must not survive into the operation's `args`.
assert.equal(normalizeAngleExpression(" π "), "π");
assert.equal(normalizeAngleExpression("\tπ / 2\n"), "π / 2");
assert.equal(normalizeAngleExpression(""), "");
});

test("normalizeAngleExpression: folds case-insensitive 'pi' → 'π'", () => {
// The π button on the prompt inserts the literal character, but
// users can also type any case variant of `pi`. Both must
// normalize to the same persisted form so `args` doesn't depend
// on which input path was used.
assert.equal(normalizeAngleExpression("pi"), "π");
assert.equal(normalizeAngleExpression("Pi"), "π");
assert.equal(normalizeAngleExpression("PI"), "π");
assert.equal(normalizeAngleExpression("pI"), "π");
});

test("normalizeAngleExpression: folds 'pi' embedded inside an expression", () => {
// The fold is unanchored — every occurrence within an
// expression is replaced. Pin the common case (a `pi` factor
// inside an arithmetic expression).
assert.equal(normalizeAngleExpression("pi / 2"), "π / 2");
assert.equal(normalizeAngleExpression("2 * Pi + PI"), "2 * π + π");
assert.equal(normalizeAngleExpression("(pi)"), "(π)");
});

test("normalizeAngleExpression: leaves already-normalized π untouched", () => {
// Idempotency — passing the output back through the normalizer
// must be a no-op. The prompt re-runs the normalize step on
// every input event, so this property is what keeps the OK
// button's enabled state stable.
assert.equal(normalizeAngleExpression("π / 2"), "π / 2");
assert.equal(normalizeAngleExpression("2 * π"), "2 * π");
assert.equal(
normalizeAngleExpression(normalizeAngleExpression("PI / 2")),
"π / 2",
);
});
Loading
Loading