Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions .changeset/ode-numeric-not-defined.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@doenet/doenetml": patch
"@doenet/standalone": patch
"@doenet/doenetml-iframe": patch
"@doenet/vscode-extension": patch
"doenet-vscode-extension": patch
---

Fix `<odeSystem>` failing with "numeric is not defined" in the browser.

Any document containing an `<odeSystem>` rendered as that error banner instead of a document, and any graph of a solution drew no curve. The solver, `dopri`, comes from numeric.js, bundled inside math-expressions. numeric builds most of its helpers at load time with the `Function` constructor, and the generated bodies reference a bare `numeric` — resolvable only if numeric has registered itself on the global object. It did that solely through Node's `global`, which neither a browser main thread nor a web worker has, so every generated helper threw the first time it was called, and `dopri` reaches them immediately. The worker's evaluation and the main-thread renderer's curve sampling both went through that path, so both failed.

Fixed upstream in math-expressions 2.0.0-alpha95, which registers numeric itself; this bumps to it.
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
"lint-staged": "^15.0.0",
"katex": "^0.16.47",
"lorem-ipsum": "^2.0.8",
"math-expressions": "^2.0.0-alpha94",
"math-expressions": "^2.0.0-alpha95",
"micromark": "^4.0.2",
"nanoid": "^5.1.16",
"nextra": "^3.3.1",
Expand Down
66 changes: 66 additions & 0 deletions packages/test-cypress/cypress/e2e/dynamicalsystem/odesystem.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { getDiagnosticsByType } from "../../support/diagnostics";

describe("ODESystem Tag Tests", { tags: ["@group2"] }, function () {
beforeEach(() => {
cy.clearIndexedDB();
cy.visit("/");
});

// Regression test for `ReferenceError: numeric is not defined`, which turned
// every `<odeSystem>` document into an error banner. The solver comes from
// numeric.js, whose generated helpers reference a bare `numeric` and so need
// numeric registered on the global object; it used to register itself only
// through Node's `global` (fixed in math-expressions 2.0.0-alpha95).
//
// That makes this a Cypress test by necessity: under Vitest `global` exists,
// numeric registers itself, and nothing fails. Both sides of the worker
// boundary have to be exercised — the worker evaluates `$$f(1)`, and the
// main-thread renderer samples the same solution to draw the curve.
it("solves and plots in the browser", () => {
cy.window().then((win) => {
win.postMessage(
{
doenetML: `
<odeSystem name="ode" initialConditions="512" tolerance="0.01" displayDigits="6">
<rightHandSide>0.0000006*x*(x-2000)(500-x)</rightHandSide>
</odeSystem>
<function name="f" extend="$ode.numericalSolution" />

<p name="pVal">value at 1: $$f(1)</p>

<graph name="g" xmin="-2" xmax="20" ymin="-300" ymax="3200">
<function extend="$f" name="curve" />
</graph>
`,
},
"*",
);
});

// Worker side: the solution evaluated at t = 1.
cy.get("#pVal").should("have.text", "value at 1: 518.99");

// Main-thread side: JSXGraph samples the solution to draw the curve, so
// a plotted path proves the renderer reached numeric too. The axes and
// their ticks are paths as well, but they are the only ones stroked in
// the axis color, so excluding that color leaves just the curve.
cy.get(`.jxgbox path:not([stroke="var(--canvasText)"])`).should(
($paths) => {
const segments = $paths
.toArray()
.map((p) => (p.getAttribute("d") || "").split("L").length);
// A sampled curve is hundreds of segments; anything the
// renderer draws without reaching the solver is a handful.
expect(
Math.max(...segments),
"segments in the longest non-axis path",
).to.be.greaterThan(100);
},
);

cy.window().then((win) => {
const { errors } = getDiagnosticsByType(win.returnDiagnostics1());
expect(errors.length).eq(0);
});
});
});
2 changes: 1 addition & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"@doenet/i18n": "*",
"color-name": "^1.1.4",
"colord": "^2.9.3",
"math-expressions": "^2.0.0-alpha93",
"math-expressions": "^2.0.0-alpha95",
"micromark": "^4.0.2"
},
"devDependencies": {
Expand Down