Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ node_modules
*.log*
*.tgz
docs
packages/jdm-editor/src/**/__screenshots__/
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@swc/core": "^1.11.21",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^22.14.1",
"@types/react": "^18.3.11",
"@types/react": "^19.1.8",
"@typescript-eslint/eslint-plugin": "^8.30.1",
"@typescript-eslint/parser": "^8.30.1",
"eslint": "9.24.0",
Expand Down
35 changes: 20 additions & 15 deletions packages/jdm-editor/package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "@gorules/jdm-editor",
"version": "1.45.0",
"description": "",
"author": "GoRules <hi@gorules.io> (https://gorules.io)",
"homepage": "https://github.com/gorules/jdm-editor",
"name": "giorules-jdm-editor",
"version": "1.46.0",
"description": "React 19 Compatible Fork of GoRules JDM Editor",
"author": "Giorgio Delgado & CoPlane Inc.",
"homepage": "https://github.com/supermacro/gio-rules",
"license": "MIT",
"keywords": [],
"type": "module",
Expand Down Expand Up @@ -35,7 +35,9 @@
"storybook": "storybook dev -p 9009",
"build:storybook": "storybook build -o docs",
"typecheck": "tsc --noEmit",
"prepublishOnly": "vite build"
"prepublishOnly": "vite build",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@ant-design/icons": "6.0.0",
Expand Down Expand Up @@ -73,11 +75,10 @@
"react-resizable-panels": "^2.1.7",
"reactflow": "11.11.4",
"to-json-schema": "^0.2.5",
"transition-hook": "^1.5.2",
"ts-pattern": "^5.7.0",
"use-debounce": "^10.0.4",
"zod": "^3.24.2",
"zustand": "^4.5.5"
"zustand": "^5.0.11"
},
"devDependencies": {
"@storybook/addon-actions": "8.6.12",
Expand All @@ -96,19 +97,23 @@
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@types/big.js": "^6.2.2",
"@types/lodash": "^4.17.16",
"@types/react": "18.3.11",
"@types/react-dom": "19.1.2",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/to-json-schema": "^0.2.4",
"@vitejs/plugin-react-swc": "^3.8.1",
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "^4.0.18",
"dayjs": "^1.11.13",
"react": "18.3.1",
"react-dom": "18.3.1",
"playwright": "^1.58.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"sass": "^1.86.3",
"storybook": "8.6.12",
"storybook-dark-mode": "^4.0.2",
"vite": "6.2.6",
"vite-plugin-dts": "^4.5.3",
"vite-plugin-wasm": "^3.4.1"
"vite-plugin-wasm": "^3.4.1",
"vitest": "^4.0.18"
},
"jest": {
"collectCoverageFrom": [
Expand All @@ -123,7 +128,7 @@
]
},
"peerDependencies": {
"react": ">= 18",
"react-dom": ">= 18"
"react": ">= 19",
"react-dom": ">= 19"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';

import { getCompletions } from '../completion';

describe('getCompletions integration', () => {
it('returns the same array reference across repeated calls (cache contract)', () => {
const first = getCompletions();
const second = getCompletions();

expect(Array.isArray(first)).toBe(true);
expect(second).toBe(first);
});

it('returns completion entries with expected shape when available', () => {
const completions = getCompletions();

if (completions.length === 0) {
expect(completions).toEqual([]);
return;
}

const first = completions[0];
expect(first).toMatchObject({
type: expect.any(String),
label: expect.any(String),
detail: expect.any(String),
info: expect.any(String),
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from 'vitest';

import { renderDiagnosticMessage } from '../diagnostic';

describe('renderDiagnosticMessage', () => {
it('renders inline code spans with string color', () => {
const result = renderDiagnosticMessage({ text: 'Type is `"usd"`', className: 'tok' });
expect(result).toContain('class="tok"');
expect(result).toContain('color: #6aab73');
expect(result).toContain('&quot;usd&quot;');
});

it('renders number code spans with number color', () => {
const result = renderDiagnosticMessage({ text: 'Value is `123`' });
expect(result).toContain('color: #57a8f5');
expect(result).toContain('123');
});

it('escapes html-sensitive characters in code spans', () => {
const result = renderDiagnosticMessage({ text: 'Bad token `<img src=x onerror=1>`' });
expect(result).toContain('&lt;img src=x onerror=1&gt;');
expect(result).not.toContain('<img src=x onerror=1>');
});

it('returns unchanged text when no backtick spans exist', () => {
const text = 'Plain text only';
expect(renderDiagnosticMessage({ text })).toBe(text);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { VariableType } from '@gorules/zen-engine-wasm';
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { describe, expect, it } from 'vitest';

import { isWasmAvailable } from '../../../../helpers/wasm';
import { validateZenExpression } from '../linter';

describe('validateZenExpression integration', () => {
it('returns no diagnostics for empty source', () => {
expect(
validateZenExpression({
source: ' ',
expressionType: 'standard',
types: [],
}),
).toEqual([]);
});

it('maps type diagnostics severities from hint/info/warning prefixes', () => {
const diagnostics = validateZenExpression({
source: 'amount',
types: [
{ error: 'Hint: consider cast', kind: 'Any', nodeKind: 'Expr', span: [0, 6] },
{ error: 'Info: this narrows type', kind: 'Any', nodeKind: 'Expr', span: [0, 6] },
{ error: 'Type mismatch', kind: 'Any', nodeKind: 'Expr', span: [0, 6] },
],
});

const severities = diagnostics.map((d) => d.severity);
expect(severities).toContain('hint');
expect(severities).toContain('info');
expect(severities).toContain('warning');
});

it('enforces unary bool result with strictness-aware severity', () => {
const warningDiagnostics = validateZenExpression({
source: 'amount',
expressionType: 'unary',
strict: false,
types: [{ error: null, kind: 'Number', nodeKind: 'Expr', span: [0, 6] }],
});

const errorDiagnostics = validateZenExpression({
source: 'amount',
expressionType: 'unary',
strict: true,
types: [{ error: null, kind: 'Number', nodeKind: 'Expr', span: [0, 6] }],
});

const warningUnary = warningDiagnostics.find((d) => d.message.includes('Expected unary expression'));
const errorUnary = errorDiagnostics.find((d) => d.message.includes('Expected unary expression'));

expect(warningUnary?.severity).toBe('warning');
expect(errorUnary?.severity).toBe('error');
});

it('reports expected type mismatch and attaches renderMessage', () => {
const expected = VariableType.fromJson('Bool');
const diagnostics = validateZenExpression({
source: 'amount',
strict: true,
types: [{ error: null, kind: 'Any', nodeKind: 'Expr', span: [0, 6] }],
expectedVariableType: expected,
});

const mismatch = diagnostics.find((d) => d.message.includes('Expected expression to evaluate to type'));
expect(mismatch?.severity).toBe('error');

const host = document.createElement('div');
document.body.appendChild(host);
const view = new EditorView({
state: EditorState.create({ doc: '' }),
parent: host,
});

const rendered = mismatch?.renderMessage?.(view);
expect(rendered instanceof HTMLElement).toBe(true);
expect(rendered?.textContent).toContain('Expected expression to evaluate to type');

view.destroy();
host.remove();
});

it('returns parser diagnostics for invalid syntax when wasm is available', () => {
const diagnostics = validateZenExpression({
source: 'a +',
expressionType: 'standard',
types: [],
});

if (!isWasmAvailable()) {
expect(diagnostics).toEqual([]);
return;
}

expect(diagnostics.length).toBeGreaterThan(0);
expect(
diagnostics.some((d) => ['Parser error', 'Lexer error', 'Compiler error', 'VM error'].includes(d.source ?? '')),
).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { EditorState } from '@codemirror/state';
import { createVariableType } from '@gorules/zen-engine-wasm';
import { describe, expect, it } from 'vitest';

import {
buildTypeCompletion,
typeField,
updateExpectedVariableTypeEffect,
updateExpressionTypeEffect,
updateStrictModeEffect,
updateVariableTypeEffect,
zenKindToString,
} from '../types';

const createTestVariableType = () =>
createVariableType({
Object: {
customer: { Object: { id: 'String' } },
amount: 'Number',
},
});

describe('types helpers', () => {
it('buildTypeCompletion builds object property completions', () => {
const completions = buildTypeCompletion({ kind: { Object: { amount: 'Number', approved: 'Bool' } }, type: 'variable' });
expect(completions).toHaveLength(2);
expect(completions[0]).toMatchObject({ label: 'amount', type: 'variable', detail: 'number' });
});

it('zenKindToString converts primitive and structured kinds', () => {
expect(zenKindToString('String')).toBe('string');
expect(zenKindToString({ Array: 'Bool' })).toBe('bool[]');
expect(zenKindToString({ Const: 'usd' })).toBe('"usd"');
expect(zenKindToString({ Enum: ['usd', 'eur'] })).toBe('"usd" | "eur"');
});

it('typeField computes type info on variable type update and doc change', () => {
const variableType = createTestVariableType();
let state = EditorState.create({ doc: 'amount', extensions: [typeField] });

state = state.update({
changes: { from: 0, to: state.doc.length, insert: 'amount' },
effects: [updateVariableTypeEffect.of(variableType)],
}).state;

const field = state.field(typeField);
expect(field.rootKind).toEqual({ Object: { customer: { Object: { id: 'String' } }, amount: 'Number' } });
expect(field.types[0]?.kind).toBe('Number');
});

it('typeField supports unary mode, strict mode, and expected type updates', () => {
const variableType = createTestVariableType();
let state = EditorState.create({ doc: 'amount > 10', extensions: [typeField] });

state = state.update({ effects: [updateVariableTypeEffect.of(variableType)] }).state;
state = state.update({ effects: [updateExpressionTypeEffect.of('unary'), updateStrictModeEffect.of(true)] }).state;
state = state.update({ effects: [updateExpectedVariableTypeEffect.of(variableType)] }).state;

const field = state.field(typeField);
expect(field.expressionType).toBe('unary');
expect(field.strict).toBe(true);
expect(field.expectedVariableType).toBe(variableType);
});
});
Loading