1+ // tests-e2e/calculator.spec.js
2+ //
3+ // Regression coverage for GitHub issue #1590:
4+ // "[Bug]: Scientific Calculator breaks when using exp()"
5+ //
6+ // compileMathFunction() replaced function names like "exp(" with
7+ // "Math.exp(" via one regex, and *separately* replaced every literal "e"
8+ // character with "Math.E" via another regex, intended for the Euler's
9+ // number constant. But an earlier "implicit multiplication" step ran
10+ // first and tokenized character-by-character: it matched the "e" at the
11+ // start of "exp(1)", then greedily matched the "x" right after it as a
12+ // second token, splitting "exp(1)" into "e" * "xp(1)" before the
13+ // "exp(" -> "Math.exp(" rule ever got a chance to run. The final constant
14+ // substitution then turned the leftover "e" into "Math.E", producing the
15+ // nonsensical "Math.E*xp(1)" and a ReferenceError, surfaced to the user
16+ // as "Invalid Expression".
17+ //
18+ // The fix protects every function name (sin, cos, tan, sqrt, log, ln,
19+ // abs, exp) behind an opaque placeholder *before* any implicit-
20+ // multiplication or constant-substitution regex runs, so "exp(" can never
21+ // be torn apart into "e" + "xp(" again.
22+ //
23+ // Assumes the Playwright test runner (`@playwright/test`) is used for
24+ // files under tests-e2e/, matching the .spec.js naming convention.
25+
26+ const { test, expect } = require ( '@playwright/test' ) ;
27+ const fs = require ( 'fs' ) ;
28+ const path = require ( 'path' ) ;
29+
30+ const SCRIPT_PATH = path . join ( __dirname , '..' , 'js' , 'projects' , 'calculator.js' ) ;
31+ const calculatorSource = fs . readFileSync ( SCRIPT_PATH , 'utf-8' ) ;
32+
33+ const HARNESS_HTML = `
34+ <!DOCTYPE html>
35+ <html>
36+ <head>
37+ <style>
38+ :root {
39+ --surface-color: #ffffff;
40+ --border-color: #cccccc;
41+ --text-color: #111111;
42+ --text-secondary: #666666;
43+ --primary-color: #333333;
44+ --text: #111111;
45+ }
46+ </style>
47+ </head>
48+ <body>
49+ <div id="app"></div>
50+ <script>${ calculatorSource } </script>
51+ </body>
52+ </html>
53+ ` ;
54+
55+ async function evaluate ( page , expression ) {
56+ await page . fill ( '#calcInput' , expression ) ;
57+ await page . click ( '.calc-btn.equals' ) ;
58+ // A committed evaluation replaces the input's own value with the
59+ // cleaned numeric result (see evaluateStandard's commit branch), and
60+ // clears the secondary live-preview line.
61+ return page . inputValue ( '#calcInput' ) ;
62+ }
63+
64+ test . describe ( 'Scientific Calculator - exp() parsing (issue #1590)' , ( ) => {
65+ test ( 'source protects function names with placeholders before constant substitution' , ( ) => {
66+ // Guards directly against the regression: dropping the placeholder
67+ // step (or reordering it after the implicit-multiplication / "e"
68+ // constant regexes) reintroduces the exact bug from issue #1590.
69+ expect ( calculatorSource ) . toMatch ( / F U N C S \. f o r E a c h / ) ;
70+ expect ( calculatorSource ) . toMatch ( / p l a c e h o l d e r \( / ) ;
71+ } ) ;
72+
73+ test . beforeEach ( async ( { page } ) => {
74+ await page . setContent ( HARNESS_HTML ) ;
75+ await page . evaluate ( ( ) => {
76+ document . getElementById ( 'app' ) . innerHTML = getCalculatorHTML ( ) ;
77+ initCalculator ( ) ;
78+ } ) ;
79+ } ) ;
80+
81+ test ( 'exp(1) evaluates to Euler\u2019s number, not "Invalid Expression"' , async ( { page } ) => {
82+ const value = await evaluate ( page , 'exp(1)' ) ;
83+ expect ( Number ( value ) ) . toBeCloseTo ( Math . E , 9 ) ;
84+ } ) ;
85+
86+ test ( 'exp(0) evaluates to 1' , async ( { page } ) => {
87+ const value = await evaluate ( page , 'exp(0)' ) ;
88+ expect ( Number ( value ) ) . toBe ( 1 ) ;
89+ } ) ;
90+
91+ test ( 'implicit multiplication before exp still works: 3exp(1)' , async ( { page } ) => {
92+ const value = await evaluate ( page , '3exp(1)' ) ;
93+ expect ( Number ( value ) ) . toBeCloseTo ( 3 * Math . E , 6 ) ;
94+ } ) ;
95+
96+ test ( 'nested exp() calls work: exp(exp(1))' , async ( { page } ) => {
97+ const value = await evaluate ( page , 'exp(exp(1))' ) ;
98+ expect ( Number ( value ) ) . toBeCloseTo ( Math . exp ( Math . E ) , 6 ) ;
99+ } ) ;
100+
101+ test ( 'exp() combined with the standalone "e" constant: exp(1)+e' , async ( { page } ) => {
102+ const value = await evaluate ( page , 'exp(1)+e' ) ;
103+ expect ( Number ( value ) ) . toBeCloseTo ( Math . E + Math . E , 6 ) ;
104+ } ) ;
105+
106+ test ( 'the standalone "e" constant still evaluates correctly on its own' , async ( { page } ) => {
107+ const value = await evaluate ( page , 'e^2' ) ;
108+ expect ( Number ( value ) ) . toBeCloseTo ( Math . E ** 2 , 6 ) ;
109+ } ) ;
110+
111+ test ( 'other scientific functions are unaffected by the fix' , async ( { page } ) => {
112+ expect ( Number ( await evaluate ( page , 'sin(0)' ) ) ) . toBe ( 0 ) ;
113+ expect ( Number ( await evaluate ( page , 'sqrt(4)' ) ) ) . toBe ( 2 ) ;
114+ expect ( Number ( await evaluate ( page , 'log(100)' ) ) ) . toBe ( 2 ) ;
115+ expect ( Number ( await evaluate ( page , 'ln(e)' ) ) ) . toBeCloseTo ( 1 , 9 ) ;
116+ } ) ;
117+ } ) ;
0 commit comments