This commit is contained in:
+97
@@ -0,0 +1,97 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`test Key Path Expression V2 > init const test_key_path = source.a 1`] = `
|
||||
{
|
||||
"initializer": {
|
||||
"keyPath": [
|
||||
"source",
|
||||
"a",
|
||||
],
|
||||
"kind": "KeyPathExpression",
|
||||
},
|
||||
"key": "test_key_path",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 0,
|
||||
"type": {
|
||||
"kind": "String",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test Key Path Expression V2 > subscribe variable changes by expression 1`] = `
|
||||
[
|
||||
1,
|
||||
{
|
||||
"kind": "Boolean",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Key Path Expression V2 > subscribe variable changes by expression 2`] = `
|
||||
[
|
||||
2,
|
||||
{
|
||||
"kind": "Number",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Key Path Expression V2 > subscribe variable changes by expression 3`] = `
|
||||
[
|
||||
3,
|
||||
undefined,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Key Path Expression V2 > subscribe variable changes by expression 4`] = `
|
||||
[
|
||||
4,
|
||||
{
|
||||
"kind": "Number",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Key Path Expression V2 > subscribe variable changes by expression 5`] = `
|
||||
[
|
||||
5,
|
||||
undefined,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Key Path Expression V2 > subscribe variable changes by expression 6`] = `
|
||||
[
|
||||
6,
|
||||
{
|
||||
"kind": "Number",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Key Path Expression V2 > subscribe variable changes by expression 7`] = `
|
||||
{
|
||||
"initializer": {
|
||||
"enumerateFor": {
|
||||
"keyPath": [
|
||||
"source",
|
||||
"b",
|
||||
],
|
||||
"kind": "KeyPathExpression",
|
||||
},
|
||||
"kind": "EnumerateExpression",
|
||||
},
|
||||
"key": "test_key_path",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 0,
|
||||
"type": {
|
||||
"kind": "Number",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test Key Path Expression V2 > subscribe variable changes by expression 8`] = `
|
||||
[
|
||||
7,
|
||||
undefined,
|
||||
]
|
||||
`;
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`test Basic Variable Declaration > test globalVariableTable variables 1`] = `
|
||||
[
|
||||
"string",
|
||||
"boolean",
|
||||
"number",
|
||||
"integer",
|
||||
"object",
|
||||
"map",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Basic Variable Declaration > test remove variable, update variable, add variable by from a new json 1`] = `
|
||||
[
|
||||
"object",
|
||||
"new_integer",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Basic Variable Declaration > test remove variable, update variable, add variable by from a new json 2`] = `[]`;
|
||||
|
||||
exports[`test Basic Variable Declaration > test simple variable declarations 1`] = `
|
||||
{
|
||||
"declarations": [
|
||||
{
|
||||
"key": "string",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 0,
|
||||
"type": {
|
||||
"kind": "String",
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "boolean",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 1,
|
||||
"type": {
|
||||
"kind": "Boolean",
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "number",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 2,
|
||||
"type": {
|
||||
"kind": "Number",
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "integer",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 3,
|
||||
"type": {
|
||||
"kind": "Integer",
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "object",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 4,
|
||||
"type": {
|
||||
"kind": "Object",
|
||||
"properties": [
|
||||
{
|
||||
"key": "key1",
|
||||
"kind": "Property",
|
||||
"type": {
|
||||
"kind": "String",
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "key2",
|
||||
"kind": "Property",
|
||||
"type": {
|
||||
"kind": "Object",
|
||||
"properties": [
|
||||
{
|
||||
"key": "key1",
|
||||
"kind": "Property",
|
||||
"type": {
|
||||
"kind": "Number",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "key3",
|
||||
"kind": "Property",
|
||||
"type": {
|
||||
"kind": "Array",
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "key4",
|
||||
"kind": "Property",
|
||||
"type": {
|
||||
"items": {
|
||||
"kind": "Object",
|
||||
"properties": [
|
||||
{
|
||||
"key": "key1",
|
||||
"kind": "Property",
|
||||
"type": {
|
||||
"kind": "Boolean",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"kind": "Array",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "map",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 5,
|
||||
"type": {
|
||||
"keyType": {
|
||||
"kind": "String",
|
||||
},
|
||||
"kind": "Map",
|
||||
"valueType": {
|
||||
"kind": "Number",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"kind": "VariableDeclarationList",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test Basic Variable Declaration > variable declaration subscribe 1`] = `
|
||||
{
|
||||
"kind": "Number",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test Basic Variable Declaration > variable declaration subscribe 2`] = `
|
||||
{
|
||||
"kind": "Object",
|
||||
"properties": [
|
||||
{
|
||||
"key": "key1",
|
||||
"kind": "Property",
|
||||
"type": {
|
||||
"kind": "String",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test Basic Variable Declaration > variable declaration subscribe 3`] = `
|
||||
{
|
||||
"kind": "Object",
|
||||
"properties": [
|
||||
{
|
||||
"key": "key1",
|
||||
"kind": "Property",
|
||||
"type": {
|
||||
"kind": "Number",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`test Variable With Initializer > init const test_key_path = source.a 1`] = `
|
||||
{
|
||||
"initializer": {
|
||||
"keyPath": [
|
||||
"source",
|
||||
"a",
|
||||
],
|
||||
"kind": "KeyPathExpression",
|
||||
},
|
||||
"key": "test_key_path",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 0,
|
||||
"type": {
|
||||
"kind": "String",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test Variable With Initializer > subscribe variable changes by expression 1`] = `
|
||||
[
|
||||
1,
|
||||
{
|
||||
"kind": "Boolean",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Variable With Initializer > subscribe variable changes by expression 2`] = `
|
||||
[
|
||||
2,
|
||||
{
|
||||
"kind": "Number",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Variable With Initializer > subscribe variable changes by expression 3`] = `
|
||||
[
|
||||
3,
|
||||
undefined,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Variable With Initializer > subscribe variable changes by expression 4`] = `
|
||||
[
|
||||
4,
|
||||
{
|
||||
"kind": "Number",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Variable With Initializer > subscribe variable changes by expression 5`] = `
|
||||
[
|
||||
5,
|
||||
undefined,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Variable With Initializer > subscribe variable changes by expression 6`] = `
|
||||
[
|
||||
6,
|
||||
{
|
||||
"kind": "Number",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`test Variable With Initializer > subscribe variable changes by expression 7`] = `
|
||||
{
|
||||
"initializer": {
|
||||
"enumerateFor": {
|
||||
"keyPath": [
|
||||
"source",
|
||||
"b",
|
||||
],
|
||||
"kind": "KeyPathExpression",
|
||||
},
|
||||
"kind": "EnumerateExpression",
|
||||
},
|
||||
"key": "test_key_path",
|
||||
"kind": "VariableDeclaration",
|
||||
"order": 0,
|
||||
"type": {
|
||||
"kind": "Number",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`test Variable With Initializer > subscribe variable changes by expression 8`] = `
|
||||
[
|
||||
7,
|
||||
undefined,
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { VariableEngine } from '../../src/variable-engine';
|
||||
import { ASTNode, postConstructAST } from '../../src/ast';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
describe('test ast decorators', () => {
|
||||
const container = getContainer();
|
||||
|
||||
const variableEngine: VariableEngine = container.get(VariableEngine);
|
||||
|
||||
const testScope = variableEngine.createScope('test');
|
||||
|
||||
test('@postConstructAST()', () => {
|
||||
let postConstructCalled = false;
|
||||
|
||||
class PostConstructTest extends ASTNode {
|
||||
static kind = 'PostConstructTest';
|
||||
|
||||
testFlag = false;
|
||||
|
||||
@postConstructAST()
|
||||
init() {
|
||||
postConstructCalled = true;
|
||||
this.testFlag = true;
|
||||
}
|
||||
|
||||
fromJSON(json: any): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
testFlag: this.testFlag,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
variableEngine.astRegisters.registerAST(PostConstructTest);
|
||||
const ast: PostConstructTest = testScope.ast.set('test', {
|
||||
kind: 'PostConstructTest',
|
||||
});
|
||||
const ast2: PostConstructTest = testScope.ast.set('test', {
|
||||
kind: 'PostConstructTest',
|
||||
});
|
||||
|
||||
expect(postConstructCalled).toBeTruthy();
|
||||
expect(ast.testFlag).toBeTruthy();
|
||||
expect(ast2.testFlag).toBeTruthy();
|
||||
});
|
||||
|
||||
test('@postConstructAST() Annotate Only One time', () => {
|
||||
expect(() => {
|
||||
class PostConstructTest2 extends ASTNode {
|
||||
static kind = 'PostConstructTest2';
|
||||
|
||||
testFlag1 = false;
|
||||
|
||||
testFlag2 = false;
|
||||
|
||||
@postConstructAST()
|
||||
init() {
|
||||
this.testFlag1 = true;
|
||||
}
|
||||
|
||||
@postConstructAST()
|
||||
init2() {
|
||||
this.testFlag2 = true;
|
||||
}
|
||||
|
||||
fromJSON(json: any): void {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
testFlag1: this.testFlag1,
|
||||
testFlag2: this.testFlag2,
|
||||
};
|
||||
}
|
||||
}
|
||||
variableEngine.astRegisters.registerAST(PostConstructTest2);
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { vi, describe, test, expect } from 'vitest';
|
||||
|
||||
import { getParentFields } from '../../src/ast/utils/variable-field';
|
||||
import { ASTKind, VariableEngine, VariableDeclaration, ASTFactory } from '../../src';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
const {
|
||||
createVariableDeclaration,
|
||||
createObject,
|
||||
createProperty,
|
||||
createString,
|
||||
createNumber,
|
||||
createKeyPathExpression,
|
||||
createArray,
|
||||
createEnumerateExpression,
|
||||
} = ASTFactory;
|
||||
|
||||
vi.mock('nanoid', () => {
|
||||
let mockId = 0;
|
||||
return {
|
||||
nanoid: () => 'mocked-id-' + mockId++,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试通过表达式生成的变量
|
||||
*/
|
||||
describe('test Key Path Expression V2', () => {
|
||||
const container = getContainer();
|
||||
const variableEngine = container.get(VariableEngine);
|
||||
const globalScope = variableEngine.createScope('global');
|
||||
const testScope = variableEngine.createScope('test');
|
||||
|
||||
const globalTestVariable = globalScope.ast.set<VariableDeclaration>(
|
||||
'test',
|
||||
createVariableDeclaration({
|
||||
key: 'source',
|
||||
type: createObject({
|
||||
properties: [
|
||||
createProperty({
|
||||
key: 'a',
|
||||
type: createString(),
|
||||
}),
|
||||
createProperty({
|
||||
key: 'b',
|
||||
type: createNumber(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
test('init const test_key_path = source.a', () => {
|
||||
const variableByKeyPath = testScope.ast.set<VariableDeclaration>(
|
||||
'variableByKeyPath',
|
||||
createVariableDeclaration({
|
||||
key: 'test_key_path',
|
||||
initializer: createKeyPathExpression({
|
||||
keyPath: ['source', 'a'],
|
||||
}),
|
||||
})
|
||||
)!;
|
||||
|
||||
expect(variableByKeyPath.initializer?.kind).toEqual(ASTKind.KeyPathExpression);
|
||||
expect(variableByKeyPath.initializer?.refs.map((_ref) => _ref?.key)).toEqual(['a']);
|
||||
// variableByKeyPath 的类型重新建立了实例,其父节点为当前节点
|
||||
expect(getParentFields(variableByKeyPath.type).map((_field) => _field?.key)).toEqual([
|
||||
'test_key_path',
|
||||
]);
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.String });
|
||||
expect(variableByKeyPath.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('subscribe variable changes by expression', () => {
|
||||
// const variableByKeyPath = source.a;
|
||||
const variableByKeyPath = testScope.output.getVariableByKey('test_key_path')!;
|
||||
|
||||
let typeChangeTimes = 0;
|
||||
variableByKeyPath.onTypeChange((type) => {
|
||||
typeChangeTimes++;
|
||||
expect([typeChangeTimes, type?.toJSON()]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// source.a 发生变化时,则响应更新变量
|
||||
// source.a -> boolean
|
||||
globalTestVariable.getByKeyPath(['a'])?.updateType(ASTKind.Boolean);
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Boolean });
|
||||
expect(typeChangeTimes).toBe(1);
|
||||
|
||||
// 改成引用 source.b,响应更新变量
|
||||
// const variableByKeyPath = source.b;
|
||||
variableByKeyPath.updateInitializer({
|
||||
kind: ASTKind.KeyPathExpression,
|
||||
keyPath: ['source', 'b'],
|
||||
});
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
|
||||
expect(typeChangeTimes).toBe(2);
|
||||
|
||||
// source.b 被删除,变量类型变为空
|
||||
globalTestVariable.type.fromJSON({
|
||||
properties: [
|
||||
{
|
||||
key: 'a',
|
||||
type: ASTKind.String,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(variableByKeyPath.type).toBeUndefined();
|
||||
expect(typeChangeTimes).toBe(3);
|
||||
|
||||
// source.b 加回来,变量类型变回来且触发表达式更新
|
||||
globalTestVariable.type.fromJSON(
|
||||
createObject({
|
||||
properties: [
|
||||
createProperty({ key: 'a', type: createString() }),
|
||||
createProperty({ key: 'b', type: createNumber() }),
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
|
||||
expect(typeChangeTimes).toBe(4);
|
||||
|
||||
// 改成 EnumerateExpression
|
||||
variableByKeyPath.updateInitializer(
|
||||
createEnumerateExpression({
|
||||
enumerateFor: createKeyPathExpression({
|
||||
keyPath: ['source', 'b'],
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(variableByKeyPath.type).toBeUndefined();
|
||||
expect(variableByKeyPath.initializer?.refs).toEqual([]);
|
||||
variableByKeyPath.initializer?.refreshRefs();
|
||||
expect(variableByKeyPath.initializer?.refs).toEqual([]);
|
||||
expect(typeChangeTimes).toBe(5);
|
||||
|
||||
// 数组下钻,类型也会更新
|
||||
globalTestVariable.type.fromJSON(
|
||||
createObject({
|
||||
properties: [
|
||||
createProperty({
|
||||
key: 'b',
|
||||
type: createArray({
|
||||
items: createNumber(),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
})
|
||||
);
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
|
||||
expect(typeChangeTimes).toBe(6);
|
||||
expect(variableByKeyPath.toJSON()).toMatchSnapshot();
|
||||
|
||||
// 原作用域删除,显示为空类型
|
||||
globalScope.dispose();
|
||||
expect(variableByKeyPath.scope.depScopes.map((_scope) => _scope.id)).toEqual([]);
|
||||
expect(variableByKeyPath.type).toBeUndefined();
|
||||
expect(typeChangeTimes).toBe(7);
|
||||
});
|
||||
|
||||
const cycle1 = variableEngine.createScope('cycle1');
|
||||
const cycle2 = variableEngine.createScope('cycle2');
|
||||
const cycle3 = variableEngine.createScope('cycle3');
|
||||
|
||||
test('cycle scope with normal refs', () => {
|
||||
const cycle1Var = cycle1.ast.set<VariableDeclaration>(
|
||||
'var',
|
||||
createVariableDeclaration({
|
||||
key: 'cycle1_var',
|
||||
type: createObject({
|
||||
properties: [
|
||||
createProperty({
|
||||
key: 'a',
|
||||
type: createArray({
|
||||
items: createString(),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const cycle2Var = cycle2.ast.set<VariableDeclaration>(
|
||||
'var',
|
||||
createVariableDeclaration({
|
||||
key: 'cycle2_var',
|
||||
initializer: createEnumerateExpression({
|
||||
enumerateFor: createKeyPathExpression({
|
||||
keyPath: ['cycle1_var', 'a'],
|
||||
}),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const cycle3Var = cycle3.ast.set<VariableDeclaration>(
|
||||
'var',
|
||||
createVariableDeclaration({
|
||||
key: 'cycle3_var',
|
||||
initializer: createKeyPathExpression({
|
||||
keyPath: ['cycle2_var'],
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
expect(cycle1Var.type.toJSON()).toEqual({
|
||||
kind: ASTKind.Object,
|
||||
properties: [
|
||||
{
|
||||
kind: ASTKind.Property,
|
||||
key: 'a',
|
||||
type: {
|
||||
kind: ASTKind.Array,
|
||||
items: { kind: ASTKind.String },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(cycle2Var.type.toJSON()).toEqual({
|
||||
kind: ASTKind.String,
|
||||
});
|
||||
expect(cycle3Var.type.toJSON()).toEqual({
|
||||
kind: ASTKind.String,
|
||||
});
|
||||
});
|
||||
|
||||
test('cycle scope with child ref', () => {
|
||||
const cycle1Var = cycle1.ast.get('var')! as VariableDeclaration;
|
||||
const cycle2Var = cycle2.ast.get('var')! as VariableDeclaration;
|
||||
const cycle3Var = cycle3.ast.get('var')! as VariableDeclaration;
|
||||
|
||||
cycle1Var.fromJSON({
|
||||
type: createObject({
|
||||
properties: [
|
||||
createProperty({
|
||||
key: 'a',
|
||||
type: createArray({
|
||||
items: createString(),
|
||||
}),
|
||||
}),
|
||||
createProperty({
|
||||
key: 'b',
|
||||
initializer: createKeyPathExpression({
|
||||
keyPath: ['cycle3_var'],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(cycle1Var.type.toJSON()).toEqual({
|
||||
kind: ASTKind.Object,
|
||||
properties: [
|
||||
{
|
||||
kind: ASTKind.Property,
|
||||
key: 'a',
|
||||
type: {
|
||||
kind: ASTKind.Array,
|
||||
items: { kind: ASTKind.String },
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: ASTKind.Property,
|
||||
key: 'b',
|
||||
initializer: {
|
||||
kind: ASTKind.KeyPathExpression,
|
||||
keyPath: ['cycle3_var'],
|
||||
},
|
||||
type: {
|
||||
kind: ASTKind.String,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(cycle2Var.type.toJSON()).toEqual({
|
||||
kind: ASTKind.String,
|
||||
});
|
||||
expect(cycle3Var.type.toJSON()).toEqual({
|
||||
kind: ASTKind.String,
|
||||
});
|
||||
});
|
||||
|
||||
test('cycle scope with cycle ref', () => {
|
||||
const cycle1Var = cycle1.ast.get('var')! as VariableDeclaration;
|
||||
const cycle2Var = cycle2.ast.get('var')! as VariableDeclaration;
|
||||
const cycle3Var = cycle3.ast.get('var')! as VariableDeclaration;
|
||||
|
||||
cycle1Var.fromJSON({
|
||||
type: createObject({
|
||||
properties: [
|
||||
createProperty({
|
||||
key: 'a',
|
||||
initializer: createKeyPathExpression({
|
||||
keyPath: ['cycle3_var'],
|
||||
}),
|
||||
}),
|
||||
createProperty({
|
||||
key: 'b',
|
||||
initializer: createKeyPathExpression({
|
||||
keyPath: ['cycle3_var'],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(cycle1Var.type.toJSON()).toEqual({
|
||||
kind: ASTKind.Object,
|
||||
properties: [
|
||||
{
|
||||
kind: ASTKind.Property,
|
||||
key: 'a',
|
||||
initializer: {
|
||||
kind: ASTKind.KeyPathExpression,
|
||||
keyPath: ['cycle3_var'],
|
||||
},
|
||||
// 发生循环引用时,type 清空
|
||||
},
|
||||
{
|
||||
kind: ASTKind.Property,
|
||||
key: 'b',
|
||||
initializer: {
|
||||
kind: ASTKind.KeyPathExpression,
|
||||
keyPath: ['cycle3_var'],
|
||||
},
|
||||
// 发生循环引用时,type 清空
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(cycle2Var.type?.toJSON()).toBeUndefined();
|
||||
expect(cycle3Var.type?.toJSON()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { vi, describe, test, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
ASTKind,
|
||||
ASTMatch,
|
||||
ObjectType,
|
||||
NumberType,
|
||||
VariableEngine,
|
||||
VariableDeclaration,
|
||||
} from '../../src';
|
||||
import { simpleVariableList } from '../../__mocks__/variables';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
vi.mock('nanoid', () => {
|
||||
let mockId = 0;
|
||||
return {
|
||||
nanoid: () => 'mocked-id-' + mockId++,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试基本的变量声明场景
|
||||
*/
|
||||
describe('test Basic Variable Declaration', () => {
|
||||
const container = getContainer();
|
||||
const variableEngine = container.get(VariableEngine);
|
||||
const globalVariableTable = variableEngine.globalVariableTable;
|
||||
const testScope = variableEngine.createScope('test');
|
||||
|
||||
test('test simple variable declarations', () => {
|
||||
const simpleCase = testScope.ast.set('simple case', simpleVariableList);
|
||||
expect(simpleCase?.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('test globalVariableTable variables', () => {
|
||||
expect(globalVariableTable.variables.map((_v) => _v.key)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('test get variable by key path', () => {
|
||||
const undefinedCases = [
|
||||
['object', 'key2', 'unExisted'],
|
||||
['unExisted'],
|
||||
['string', 'drilldownString'],
|
||||
['object', 'key1', 'drilldownString'],
|
||||
['object', 'key4', 'unExistedKeyInArray', 'key1'],
|
||||
];
|
||||
|
||||
const availableCases: [string[], string][] = [
|
||||
[['object', 'key2', 'key1'], ASTKind.Number],
|
||||
[['object', 'key4', '0', 'key1'], ASTKind.Boolean],
|
||||
];
|
||||
|
||||
availableCases.forEach(([_case, _resType]) => {
|
||||
expect(globalVariableTable.getByKeyPath(_case)?.type.kind).toEqual(_resType);
|
||||
});
|
||||
|
||||
undefinedCases.forEach((_case) => {
|
||||
expect(globalVariableTable.getByKeyPath(_case)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test('test remove variable, update variable, add variable by from a new json', () => {
|
||||
const previousVariable1 = globalVariableTable.getByKeyPath(['object', 'key2', 'key1'])!;
|
||||
const previousVariable2 = globalVariableTable.getByKeyPath(['integer'])!;
|
||||
const previousVariable3 = globalVariableTable.getByKeyPath(['object', 'key4'])!;
|
||||
|
||||
testScope.ast.set('simple case', {
|
||||
kind: ASTKind.VariableDeclarationList,
|
||||
declarations: [
|
||||
{
|
||||
kind: ASTKind.VariableDeclaration,
|
||||
type: ASTKind.Integer,
|
||||
key: 'new_integer',
|
||||
},
|
||||
{
|
||||
kind: ASTKind.VariableDeclaration,
|
||||
key: 'object',
|
||||
type: {
|
||||
kind: ASTKind.Object,
|
||||
properties: [
|
||||
{
|
||||
key: 'key2',
|
||||
type: {
|
||||
kind: ASTKind.Object,
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'key4',
|
||||
type: ASTKind.String,
|
||||
},
|
||||
{
|
||||
key: 'key5',
|
||||
type: ASTKind.Array,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(previousVariable1.disposed).toBe(true);
|
||||
expect(previousVariable2.disposed).toBe(true);
|
||||
expect(previousVariable3.disposed).toBe(false);
|
||||
expect(previousVariable3.version).toBe(1); // 更新次数为一次
|
||||
expect(testScope.ast.version).toBe(2); // 调用了两次 fromJSON,因此更新了两次
|
||||
expect(globalVariableTable.variables.map((_v) => _v.key)).toMatchSnapshot();
|
||||
expect(globalVariableTable.version).toBe(2 + 1); // 调用了两次 fromJSON + Object 变量的下钻发生变化,因此 version 是 2 + 1
|
||||
expect(testScope.available.variables.map((_v) => _v.key)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('remove variables', () => {
|
||||
let isOutputChanged = false;
|
||||
let isAnyVariableChanged = false;
|
||||
testScope.output.onDataChange(() => {
|
||||
isOutputChanged = true;
|
||||
});
|
||||
testScope.output.onAnyVariableChange(() => {
|
||||
isAnyVariableChanged = true;
|
||||
});
|
||||
|
||||
// 删除所有变量
|
||||
testScope.ast.set('simple case', {
|
||||
kind: ASTKind.VariableDeclarationList,
|
||||
declarations: [],
|
||||
});
|
||||
|
||||
expect(isOutputChanged).toBeTruthy();
|
||||
expect(isAnyVariableChanged).toBeFalsy();
|
||||
});
|
||||
|
||||
test('variable declaration subscribe', () => {
|
||||
const declaration: VariableDeclaration = testScope.ast.set('subscribeTest', {
|
||||
kind: ASTKind.VariableDeclaration,
|
||||
type: ASTKind.String,
|
||||
ui: { label: 'test Label' },
|
||||
})!;
|
||||
|
||||
let declarationChangeTimes = 0;
|
||||
let typeChangeTimes = 0;
|
||||
let outputChangeTimes = 0;
|
||||
let anyVariableChangeTimes = 0;
|
||||
|
||||
testScope.output.onDataChange(() => {
|
||||
outputChangeTimes++;
|
||||
});
|
||||
testScope.output.onAnyVariableChange(() => {
|
||||
anyVariableChangeTimes++;
|
||||
});
|
||||
declaration.subscribe(() => {
|
||||
declarationChangeTimes++;
|
||||
});
|
||||
declaration.onTypeChange((type) => {
|
||||
expect(type?.toJSON()).toMatchSnapshot();
|
||||
typeChangeTimes++;
|
||||
});
|
||||
|
||||
declaration.fromJSON({
|
||||
type: ASTKind.String,
|
||||
meta: { label: 'test New Label' },
|
||||
});
|
||||
expect(declarationChangeTimes).toBe(1);
|
||||
expect(anyVariableChangeTimes).toBe(1);
|
||||
expect(typeChangeTimes).toBe(0);
|
||||
expect(outputChangeTimes).toBe(0);
|
||||
expect(declaration.meta.label).toEqual('test New Label');
|
||||
|
||||
declaration.fromJSON({
|
||||
type: ASTKind.Number,
|
||||
meta: { label: 'test Label' },
|
||||
});
|
||||
expect(ASTMatch.is(declaration.type, NumberType)).toBeTruthy();
|
||||
expect(declarationChangeTimes).toBe(2);
|
||||
expect(anyVariableChangeTimes).toBe(2);
|
||||
expect(typeChangeTimes).toBe(1);
|
||||
expect(outputChangeTimes).toBe(0);
|
||||
expect(declaration.meta.label).toEqual('test Label');
|
||||
|
||||
declaration.fromJSON({
|
||||
type: {
|
||||
kind: ASTKind.Object,
|
||||
properties: [
|
||||
{
|
||||
key: 'key1',
|
||||
type: ASTKind.String,
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: { label: 'test Label' },
|
||||
});
|
||||
expect(ASTMatch.is(declaration.type, ObjectType)).toBeTruthy();
|
||||
expect(declarationChangeTimes).toBe(3);
|
||||
expect(anyVariableChangeTimes).toBe(3);
|
||||
expect(typeChangeTimes).toBe(2);
|
||||
expect(outputChangeTimes).toBe(0);
|
||||
|
||||
// 变量下钻字段变换类型,变量也会更新
|
||||
const key1Variable = (declaration.type as ObjectType).getByKeyPath(['key1']);
|
||||
key1Variable?.updateType(ASTKind.Number);
|
||||
expect(declarationChangeTimes).toBe(4);
|
||||
expect(anyVariableChangeTimes).toBe(4);
|
||||
expect(typeChangeTimes).toBe(3);
|
||||
expect(outputChangeTimes).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { vi, describe, test, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
ASTMatch,
|
||||
ObjectType,
|
||||
NumberType,
|
||||
VariableEngine,
|
||||
StringType,
|
||||
VariableDeclarationList,
|
||||
BooleanType,
|
||||
IntegerType,
|
||||
MapType,
|
||||
ArrayType,
|
||||
} from '../../src';
|
||||
import { simpleVariableList } from '../../__mocks__/variables';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
vi.mock('nanoid', () => {
|
||||
let mockId = 0;
|
||||
return {
|
||||
nanoid: () => 'mocked-id-' + mockId++,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试基本的变量声明场景
|
||||
*/
|
||||
describe('test Basic Variable Declaration', () => {
|
||||
const container = getContainer();
|
||||
const variableEngine = container.get(VariableEngine);
|
||||
const testScope = variableEngine.createScope('test');
|
||||
|
||||
test('test simple variable match', () => {
|
||||
const simpleCase = testScope.ast.set('simple case', simpleVariableList);
|
||||
|
||||
if (!ASTMatch.isVariableDeclarationList(simpleCase)) {
|
||||
throw new Error('simpleCase is not a VariableDeclarationList');
|
||||
}
|
||||
expect(ASTMatch.isVariableDeclarationList(simpleCase)).toBeTruthy();
|
||||
expect(ASTMatch.is(simpleCase, VariableDeclarationList)).toBeTruthy();
|
||||
|
||||
const stringDeclaration = simpleCase.declarations[0];
|
||||
const booleanDeclaration = simpleCase.declarations[1];
|
||||
const numberDeclaration = simpleCase.declarations[2];
|
||||
const integerDeclaration = simpleCase.declarations[3];
|
||||
const objectDeclaration = simpleCase.declarations[4];
|
||||
const mapDeclaration = simpleCase.declarations[5];
|
||||
const arrayProperty = testScope.output.globalVariableTable.getByKeyPath(['object', 'key4']);
|
||||
|
||||
expect(ASTMatch.isString(stringDeclaration.type)).toBeTruthy();
|
||||
expect(ASTMatch.is(stringDeclaration.type, StringType)).toBeTruthy();
|
||||
|
||||
expect(ASTMatch.isBoolean(booleanDeclaration.type)).toBeTruthy();
|
||||
expect(ASTMatch.is(booleanDeclaration.type, BooleanType)).toBeTruthy();
|
||||
|
||||
expect(ASTMatch.isNumber(numberDeclaration.type)).toBeTruthy();
|
||||
expect(ASTMatch.is(numberDeclaration.type, NumberType)).toBeTruthy();
|
||||
|
||||
expect(ASTMatch.isInteger(integerDeclaration.type)).toBeTruthy();
|
||||
expect(ASTMatch.is(integerDeclaration.type, IntegerType)).toBeTruthy();
|
||||
|
||||
expect(ASTMatch.isObject(objectDeclaration.type)).toBeTruthy();
|
||||
expect(ASTMatch.is(objectDeclaration.type, ObjectType)).toBeTruthy();
|
||||
|
||||
expect(ASTMatch.isMap(mapDeclaration.type)).toBeTruthy();
|
||||
expect(ASTMatch.is(mapDeclaration.type, MapType)).toBeTruthy();
|
||||
|
||||
if (!ASTMatch.isProperty(arrayProperty)) {
|
||||
throw new Error('arrayProperty is not a Property');
|
||||
}
|
||||
expect(ASTMatch.isArray(arrayProperty.type)).toBeTruthy();
|
||||
expect(ASTMatch.is(arrayProperty.type, ArrayType)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { VariableEngine } from '../../src/variable-engine';
|
||||
import { ASTFactory } from '../../src/ast';
|
||||
import { simpleVariableList } from '../../__mocks__/variables';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
describe('Test Variable ThrowError', () => {
|
||||
const container = getContainer();
|
||||
const variableEngine = container.get(VariableEngine);
|
||||
const testScope = variableEngine.createScope('test');
|
||||
testScope.ast.set('simple case', simpleVariableList);
|
||||
|
||||
test('throw error when call getByKeyPath in base type', () => {
|
||||
const stringVariable = testScope.output.getVariableByKey('string');
|
||||
expect(() => stringVariable?.type.getByKeyPath(['throw'])).toThrowError();
|
||||
});
|
||||
|
||||
test('not throw error when variable is disposed for twice', () => {
|
||||
const stringVariable = testScope.output.getVariableByKey('string');
|
||||
|
||||
expect(stringVariable?.disposed).toBeFalsy();
|
||||
|
||||
// 删除所有变量
|
||||
testScope.ast.set(
|
||||
'simple case',
|
||||
ASTFactory.createVariableDeclarationList({ declarations: [] }),
|
||||
);
|
||||
|
||||
expect(stringVariable?.disposed).toBeTruthy();
|
||||
stringVariable?.dispose();
|
||||
expect(stringVariable?.disposed).toBeTruthy();
|
||||
});
|
||||
|
||||
test('not throw error when scope is disposed and fire its events', () => {
|
||||
const toDisposeScope = variableEngine.createScope('toDisposeTest');
|
||||
|
||||
let eventCalledTimes = 0;
|
||||
toDisposeScope.event.on('test', () => eventCalledTimes++);
|
||||
|
||||
toDisposeScope.refreshDeps();
|
||||
toDisposeScope.event.dispatch({ type: 'test' });
|
||||
expect(toDisposeScope.disposed).toBeFalsy();
|
||||
expect(eventCalledTimes).toBe(1);
|
||||
|
||||
toDisposeScope.dispose();
|
||||
expect(toDisposeScope.disposed).toBeTruthy();
|
||||
toDisposeScope.event.dispatch({ type: 'test' });
|
||||
toDisposeScope.refreshDeps();
|
||||
expect(eventCalledTimes).toBe(1);
|
||||
});
|
||||
|
||||
test('not throw error when ast is disposed and fire its events', () => {
|
||||
const toDisposeAST = testScope.ast.set(
|
||||
'dispose case',
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: 'toDispose',
|
||||
type: ASTFactory.createString(),
|
||||
}),
|
||||
);
|
||||
|
||||
let changeTimes = 0;
|
||||
toDisposeAST.subscribe(() => changeTimes++);
|
||||
|
||||
toDisposeAST.fromJSON({
|
||||
key: 'toDispose',
|
||||
type: ASTFactory.createNumber(),
|
||||
});
|
||||
expect(changeTimes).toBe(1);
|
||||
expect(toDisposeAST.disposed).toBeFalsy();
|
||||
|
||||
testScope.ast.remove('dispose case');
|
||||
expect(toDisposeAST.disposed).toBeTruthy();
|
||||
toDisposeAST.fireChange();
|
||||
expect(changeTimes).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { VariableEngine } from '../../src/variable-engine';
|
||||
import { ASTFactory, ASTKind, CustomType, VariableDeclaration } from '../../src/ast';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
const {
|
||||
createObject,
|
||||
createNumber,
|
||||
createInteger,
|
||||
createBoolean,
|
||||
createString,
|
||||
createArray,
|
||||
createCustomType,
|
||||
createMap,
|
||||
createProperty,
|
||||
createUnion,
|
||||
create,
|
||||
} = ASTFactory;
|
||||
|
||||
describe('Test Variable Type Equal', () => {
|
||||
const container = getContainer();
|
||||
const variableEngine = container.get(VariableEngine);
|
||||
const testScope = variableEngine.createScope('test');
|
||||
|
||||
const testObject1 = createObject({
|
||||
properties: [
|
||||
createProperty({ key: 'a', type: createString() }),
|
||||
createProperty({ key: 'b', type: createNumber() }),
|
||||
createProperty({ key: 'c', type: createBoolean() }),
|
||||
createProperty({ key: 'd', type: createObject({}) }),
|
||||
createProperty({ key: 'e', type: createArray({}) }),
|
||||
createProperty({ key: 'f', type: createMap({}) }),
|
||||
createProperty({ key: 'g', type: createCustomType({ typeName: 'Custom' }) }),
|
||||
],
|
||||
});
|
||||
|
||||
const testObject2 = createObject({
|
||||
properties: [
|
||||
createProperty({ key: 'a', type: createString() }),
|
||||
createProperty({ key: 'b', type: createInteger() }),
|
||||
createProperty({ key: 'c', type: createBoolean() }),
|
||||
],
|
||||
});
|
||||
|
||||
const testObject3 = createObject({
|
||||
properties: [
|
||||
createProperty({ key: 'a', type: createString() }),
|
||||
createProperty({ key: 'b', type: testObject2 }),
|
||||
createProperty({
|
||||
key: 'c',
|
||||
type: createArray({
|
||||
items: testObject2,
|
||||
}),
|
||||
}),
|
||||
createProperty({
|
||||
key: 'd',
|
||||
type: createArray({ items: createString() }),
|
||||
}),
|
||||
createProperty({
|
||||
key: 'e',
|
||||
type: createMap({
|
||||
valueType: createNumber(),
|
||||
}),
|
||||
}),
|
||||
createProperty({
|
||||
key: 'f',
|
||||
type: createMap({
|
||||
valueType: createString(),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const variable1: VariableDeclaration = testScope.ast.set('variable1', {
|
||||
kind: ASTKind.VariableDeclaration,
|
||||
key: 'variable1',
|
||||
type: testObject1,
|
||||
});
|
||||
|
||||
const variable2: VariableDeclaration = testScope.ast.set('variable2', {
|
||||
kind: ASTKind.VariableDeclaration,
|
||||
key: 'variable2',
|
||||
type: testObject2,
|
||||
});
|
||||
|
||||
const variable3: VariableDeclaration = testScope.ast.set('variable3', {
|
||||
kind: ASTKind.VariableDeclaration,
|
||||
key: 'variable3',
|
||||
type: testObject3,
|
||||
});
|
||||
|
||||
test('Test Simple Equal', () => {
|
||||
expect(variable1.type.isTypeEqual(testObject1)).toBeTruthy();
|
||||
expect(variable2.type.isTypeEqual(testObject2)).toBeTruthy();
|
||||
expect(variable3.type.isTypeEqual(testObject3)).toBeTruthy();
|
||||
|
||||
expect(variable1.type.isTypeEqual(testObject2)).toBeFalsy();
|
||||
expect(variable2.type.isTypeEqual(testObject3)).toBeFalsy();
|
||||
});
|
||||
|
||||
test('Test Union Type Equal', () => {
|
||||
expect(
|
||||
variable1.type.isTypeEqual(
|
||||
createUnion({
|
||||
types: [testObject1, testObject2, testObject3],
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
variable2.type.isTypeEqual(
|
||||
createUnion({
|
||||
types: [testObject1, testObject2, testObject3],
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
variable3.type.isTypeEqual(
|
||||
createUnion({
|
||||
types: [testObject1, testObject2, testObject3],
|
||||
})
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(variable1.type.isTypeEqual({ kind: ASTKind.Union })).toBeFalsy();
|
||||
});
|
||||
|
||||
test('Test Union Type Equal In Child Properties', () => {
|
||||
const UnionBasicTypes = createUnion({
|
||||
types: [ASTKind.String, ASTKind.Number, ASTKind.Boolean],
|
||||
});
|
||||
|
||||
expect(
|
||||
variable2.type.isTypeEqual({
|
||||
kind: ASTKind.Object,
|
||||
properties: [
|
||||
{
|
||||
key: 'a',
|
||||
type: UnionBasicTypes,
|
||||
},
|
||||
{
|
||||
key: 'b',
|
||||
type: UnionBasicTypes,
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
type: UnionBasicTypes,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('Test Weak Compare', () => {
|
||||
expect(variable1.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();
|
||||
expect(variable2.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();
|
||||
expect(variable3.type.isTypeEqual({ kind: ASTKind.Object, weak: true })).toBeTruthy();
|
||||
|
||||
expect(
|
||||
variable3.getByKeyPath(['c'])?.type.isTypeEqual({ kind: ASTKind.Array, weak: true })
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
variable3.getByKeyPath(['d'])?.type.isTypeEqual({ kind: ASTKind.Array, weak: true })
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
variable3.getByKeyPath(['e'])?.type.isTypeEqual({ kind: ASTKind.Map, weak: true })
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
variable3.getByKeyPath(['f'])?.type.isTypeEqual({ kind: ASTKind.Map, weak: true })
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('CustomType Equal', () => {
|
||||
const customType1 = createCustomType({ typeName: 'Custom' });
|
||||
const customType2 = createCustomType({ typeName: 'Custom2' });
|
||||
const customType3 = create(CustomType, { typeName: 'Custom' });
|
||||
const unionCustomTypes = createUnion({
|
||||
types: [customType1, customType2],
|
||||
});
|
||||
|
||||
const customTypeProperty = variable1.getByKeyPath(['g'])?.type;
|
||||
|
||||
expect(customTypeProperty?.isTypeEqual(customType1)).toBeTruthy();
|
||||
expect(customTypeProperty?.isTypeEqual(customType2)).toBeFalsy();
|
||||
expect(customTypeProperty?.isTypeEqual(customType3)).toBeTruthy();
|
||||
expect(customTypeProperty?.isTypeEqual(unionCustomTypes)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { vi, describe, test, expect } from 'vitest';
|
||||
|
||||
import { ASTKind, VariableEngine, VariableDeclaration, ASTFactory } from '../../src';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
const {
|
||||
createVariableDeclaration,
|
||||
createObject,
|
||||
createProperty,
|
||||
createString,
|
||||
createNumber,
|
||||
createKeyPathExpression,
|
||||
createArray,
|
||||
createEnumerateExpression,
|
||||
} = ASTFactory;
|
||||
|
||||
vi.mock('nanoid', () => {
|
||||
let mockId = 0;
|
||||
return {
|
||||
nanoid: () => 'mocked-id-' + mockId++,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试通过表达式生成的变量
|
||||
*/
|
||||
describe('test Variable With Initializer', () => {
|
||||
const container = getContainer();
|
||||
const variableEngine = container.get(VariableEngine);
|
||||
const globalScope = variableEngine.createScope('global');
|
||||
const testScope = variableEngine.createScope('test');
|
||||
|
||||
const globalTestVariable = globalScope.ast.set<VariableDeclaration>(
|
||||
'test',
|
||||
createVariableDeclaration({
|
||||
key: 'source',
|
||||
type: createObject({
|
||||
properties: [
|
||||
createProperty({
|
||||
key: 'a',
|
||||
type: createString(),
|
||||
}),
|
||||
createProperty({
|
||||
key: 'b',
|
||||
type: createNumber(),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
test('test depScopes', () => {
|
||||
expect(testScope.depScopes.map(_scope => _scope.id)).toEqual(['global']);
|
||||
});
|
||||
|
||||
test('init const test_key_path = source.a', () => {
|
||||
const variableByKeyPath = testScope.ast.set<VariableDeclaration>(
|
||||
'variableByKeyPath',
|
||||
createVariableDeclaration({
|
||||
key: 'test_key_path',
|
||||
initializer: createKeyPathExpression({
|
||||
keyPath: ['source', 'a'],
|
||||
}),
|
||||
}),
|
||||
)!;
|
||||
|
||||
expect(variableByKeyPath.initializer?.kind).toEqual(ASTKind.KeyPathExpression);
|
||||
expect(variableByKeyPath.initializer?.refs.map(_ref => _ref?.key)).toEqual(['a']);
|
||||
expect(variableByKeyPath.initializer?.parentFields.map(_field => _field?.key)).toEqual([
|
||||
'test_key_path',
|
||||
]);
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.String });
|
||||
expect(variableByKeyPath.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('subscribe variable changes by expression', () => {
|
||||
// const variableByKeyPath = source.a;
|
||||
const variableByKeyPath = testScope.output.getVariableByKey('test_key_path')!;
|
||||
|
||||
let typeChangeTimes = 0;
|
||||
variableByKeyPath.onTypeChange(type => {
|
||||
typeChangeTimes++;
|
||||
expect([typeChangeTimes, type?.toJSON()]).toMatchSnapshot();
|
||||
});
|
||||
|
||||
// source.a 发生变化时,则响应更新变量
|
||||
// source.a -> boolean
|
||||
globalTestVariable.getByKeyPath(['a'])?.updateType(ASTKind.Boolean);
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Boolean });
|
||||
expect(typeChangeTimes).toBe(1);
|
||||
|
||||
// 改成引用 source.b,响应更新变量
|
||||
// const variableByKeyPath = source.b;
|
||||
variableByKeyPath.updateInitializer({
|
||||
kind: ASTKind.KeyPathExpression,
|
||||
keyPath: ['source', 'b'],
|
||||
});
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
|
||||
expect(typeChangeTimes).toBe(2);
|
||||
|
||||
// source.b 被删除,变量类型变为空
|
||||
globalTestVariable.type.fromJSON({
|
||||
properties: [
|
||||
{
|
||||
key: 'a',
|
||||
type: ASTKind.String,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(variableByKeyPath.type).toBeUndefined();
|
||||
expect(typeChangeTimes).toBe(3);
|
||||
|
||||
// source.b 加回来,变量类型变回来且触发表达式更新
|
||||
globalTestVariable.type.fromJSON(
|
||||
createObject({
|
||||
properties: [
|
||||
createProperty({ key: 'a', type: createString() }),
|
||||
createProperty({ key: 'b', type: createNumber() }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
|
||||
expect(typeChangeTimes).toBe(4);
|
||||
|
||||
// 改成 EnumerateExpression
|
||||
variableByKeyPath.updateInitializer(
|
||||
createEnumerateExpression({
|
||||
enumerateFor: createKeyPathExpression({
|
||||
keyPath: ['source', 'b'],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(variableByKeyPath.type).toBeUndefined();
|
||||
expect(variableByKeyPath.initializer?.refs).toEqual([]);
|
||||
variableByKeyPath.initializer?.refreshRefs();
|
||||
expect(variableByKeyPath.initializer?.refs).toEqual([]);
|
||||
expect(typeChangeTimes).toBe(5);
|
||||
|
||||
// 数组下钻,类型也会更新
|
||||
globalTestVariable.type.fromJSON(
|
||||
createObject({
|
||||
properties: [
|
||||
createProperty({
|
||||
key: 'b',
|
||||
type: createArray({
|
||||
items: createNumber(),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(variableByKeyPath.type.toJSON()).toEqual({ kind: ASTKind.Number });
|
||||
expect(typeChangeTimes).toBe(6);
|
||||
expect(variableByKeyPath.toJSON()).toMatchSnapshot();
|
||||
|
||||
// 原作用域删除,显示为空类型
|
||||
globalScope.dispose();
|
||||
expect(variableByKeyPath.scope.depScopes.map(_scope => _scope.id)).toEqual([]);
|
||||
expect(variableByKeyPath.type).toBeUndefined();
|
||||
expect(typeChangeTimes).toBe(7);
|
||||
});
|
||||
});
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Container, injectable } from 'inversify';
|
||||
import { Emitter } from '@flowgram.ai/utils';
|
||||
|
||||
import { CreateASTParams } from '../../src/ast/types';
|
||||
import {
|
||||
ASTFactory,
|
||||
BaseExpression,
|
||||
BaseType,
|
||||
VariableDeclaration,
|
||||
VariableEngine,
|
||||
injectToAST,
|
||||
} from '../../src';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
interface PyExpressionJSON {
|
||||
content: string;
|
||||
uri: string;
|
||||
}
|
||||
|
||||
const delay = (timeout: number) => new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
|
||||
describe('Case Run Down: Python Expression In Blockwise', () => {
|
||||
@injectable()
|
||||
class PythonService {
|
||||
// 模拟 Blockwise 表达式后端存储推导的信息
|
||||
uriToType: Record<string, any> = {
|
||||
'blockwise://expression/a': { kind: 'String' },
|
||||
'blockwise://expression/b': { kind: 'Number' },
|
||||
};
|
||||
|
||||
// 模拟 Blockwise 单表达式推导函数
|
||||
async infer(json: PyExpressionJSON) {
|
||||
return Promise.resolve(this.uriToType[json.uri]);
|
||||
}
|
||||
|
||||
// 模拟表达式后端重新推导表达式
|
||||
batchInferEmitter = new Emitter<void>();
|
||||
|
||||
onBatchInfer = this.batchInferEmitter.event;
|
||||
}
|
||||
|
||||
class PythonExpression extends BaseExpression<PyExpressionJSON> {
|
||||
@injectToAST(PythonService) declare service: PythonService;
|
||||
|
||||
static kind: string = 'BlockwisePythonExpression';
|
||||
|
||||
_uri?: string;
|
||||
|
||||
_content?: string;
|
||||
|
||||
// Blockwise 通过 schema 变更时输出的方式来进行类型推导
|
||||
getRefFields() {
|
||||
return [];
|
||||
}
|
||||
|
||||
returnType: BaseType<any> | undefined;
|
||||
|
||||
_prevType: any;
|
||||
|
||||
// Blockwise 中 表达式通过 uri 来进行索引定位
|
||||
fromJSON(json: PyExpressionJSON): void {
|
||||
if (json.uri !== this._uri || json.content !== this._content) {
|
||||
this.service.infer(json).then((res) => {
|
||||
this._prevType = res;
|
||||
this.updateChildNodeByKey('returnType', res);
|
||||
});
|
||||
this._uri = json.uri;
|
||||
this._content = json.content;
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): PyExpressionJSON {
|
||||
return {
|
||||
content: this._content!,
|
||||
uri: this._uri!,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(params: CreateASTParams) {
|
||||
super(params);
|
||||
|
||||
this.toDispose.push(
|
||||
// 监听后端触发批量校验
|
||||
this.service.onBatchInfer(() => {
|
||||
const nextType = this.service.uriToType[this._uri!];
|
||||
|
||||
if (!isEqual(nextType, this._prevType)) {
|
||||
this.updateChildNodeByKey('returnType', nextType);
|
||||
this._prevType = nextType;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const createBlockwisePythonExpression = (json: PyExpressionJSON) => ({
|
||||
kind: 'BlockwisePythonExpression',
|
||||
...json,
|
||||
});
|
||||
|
||||
const container: Container = getContainer((bind) => {
|
||||
bind(PythonService).toSelf().inSingletonScope();
|
||||
});
|
||||
|
||||
const variableEngine: VariableEngine = container.get(VariableEngine);
|
||||
const pythonService: PythonService = container.get(PythonService);
|
||||
|
||||
variableEngine.astRegisters.registerAST(PythonExpression, () => ({
|
||||
pythonService,
|
||||
}));
|
||||
const testScope = variableEngine.createScope('test');
|
||||
|
||||
test('1. Infer When Expression Changed', async () => {
|
||||
const variable1: VariableDeclaration = testScope.ast.set(
|
||||
'variable1',
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: 'variable1',
|
||||
initializer: createBlockwisePythonExpression({
|
||||
uri: 'blockwise://expression/a',
|
||||
content: 'a + b',
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
await delay(0);
|
||||
expect(variable1.type?.kind).toEqual('String');
|
||||
expect(variable1.version).toEqual(1);
|
||||
|
||||
// 更新表达式
|
||||
pythonService.uriToType['blockwise://expression/a'] = { kind: 'Number' };
|
||||
variable1.fromJSON({
|
||||
initializer: createBlockwisePythonExpression({
|
||||
uri: 'blockwise://expression/a',
|
||||
content: 'a + b + c',
|
||||
}),
|
||||
});
|
||||
|
||||
await delay(0);
|
||||
expect(variable1.type?.kind).toEqual('Number');
|
||||
expect(variable1.version).toEqual(2);
|
||||
});
|
||||
|
||||
test('2. Infer When Global Update Triggered', async () => {
|
||||
const variable2: VariableDeclaration = testScope.ast.set(
|
||||
'variable2',
|
||||
ASTFactory.createVariableDeclaration({
|
||||
key: 'variable2',
|
||||
initializer: createBlockwisePythonExpression({
|
||||
uri: 'blockwise://expression/b',
|
||||
content: 'a + b',
|
||||
}),
|
||||
})
|
||||
);
|
||||
await delay(0);
|
||||
expect(variable2.type?.kind).toEqual('Number');
|
||||
expect(variable2.version).toEqual(1);
|
||||
|
||||
// 1. 表达式类型没有变化
|
||||
pythonService.uriToType['blockwise://expression/b'] = { kind: 'Number' };
|
||||
pythonService.batchInferEmitter.fire();
|
||||
await delay(0);
|
||||
expect(variable2.type?.kind).toEqual('Number');
|
||||
expect(variable2.version).toEqual(1);
|
||||
|
||||
// 2. 表达式类型发生了变化
|
||||
pythonService.uriToType['blockwise://expression/b'] = { kind: 'Boolean' };
|
||||
pythonService.batchInferEmitter.fire();
|
||||
await delay(0);
|
||||
expect(variable2.type?.kind).toEqual('Boolean');
|
||||
expect(variable2.version).toEqual(2);
|
||||
});
|
||||
});
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { vi, describe, test, beforeEach, expect } from 'vitest';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { ASTNode, ASTNodeFlags, VariableEngine, VariableFieldKeyRenameService } from '../../src';
|
||||
import { simpleVariableList } from '../../__mocks__/variables';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
vi.mock('nanoid', () => {
|
||||
let mockId = 0;
|
||||
return {
|
||||
nanoid: () => 'mocked-id-' + mockId++,
|
||||
};
|
||||
});
|
||||
|
||||
function getFieldKeys(node: ASTNode): string[] {
|
||||
let _curr = node?.parent;
|
||||
const parentKeys: string[] = [];
|
||||
|
||||
while (_curr) {
|
||||
if (_curr.flags & ASTNodeFlags.VariableField) {
|
||||
parentKeys.unshift(_curr.key);
|
||||
}
|
||||
_curr = _curr.parent;
|
||||
}
|
||||
|
||||
return [...parentKeys, node.key];
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试变量 Key Rename 的场景
|
||||
*/
|
||||
describe('test Listen Variable Key Rename', () => {
|
||||
const container = getContainer();
|
||||
const variableEngine = container.get(VariableEngine);
|
||||
const renameService = container.get(VariableFieldKeyRenameService);
|
||||
|
||||
const testScope = variableEngine.createScope('test');
|
||||
|
||||
let renameInfo: { before: string[]; after: string[] } | null = null;
|
||||
let disposeList: string[][] = [];
|
||||
|
||||
renameService.onRename((_rename) => {
|
||||
renameInfo = {
|
||||
before: getFieldKeys(_rename.before),
|
||||
after: getFieldKeys(_rename.after),
|
||||
};
|
||||
});
|
||||
|
||||
renameService.onDisposeInList((_field) => {
|
||||
disposeList.push(getFieldKeys(_field));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testScope.ast.set('test', simpleVariableList);
|
||||
renameInfo = null;
|
||||
disposeList = [];
|
||||
});
|
||||
|
||||
const produceNextJSON = (producer: (json: any) => void) => {
|
||||
const next = cloneDeep(simpleVariableList);
|
||||
producer(next);
|
||||
return next;
|
||||
};
|
||||
|
||||
test.each([
|
||||
// 更改一个字段
|
||||
[
|
||||
produceNextJSON((json) => {
|
||||
json.declarations[0].key = 'string1111';
|
||||
}),
|
||||
['string'],
|
||||
['string1111'],
|
||||
[],
|
||||
],
|
||||
// 更改一个下钻字段
|
||||
[
|
||||
produceNextJSON((json) => {
|
||||
json.declarations[4].type.properties[0].key = 'changedKey';
|
||||
}),
|
||||
['object', 'key1'],
|
||||
['object', 'changedKey'],
|
||||
[],
|
||||
],
|
||||
// 更改字段和他的类型
|
||||
[
|
||||
produceNextJSON((json) => {
|
||||
json.declarations[0].key = 'string1111';
|
||||
json.declarations[0].type = 'Number';
|
||||
}),
|
||||
null,
|
||||
null,
|
||||
[['string']],
|
||||
],
|
||||
// 更改多个下钻字段
|
||||
[
|
||||
produceNextJSON((json) => {
|
||||
json.declarations[4].type.properties[0].key = 'changedKey';
|
||||
json.declarations[4].type.properties[1].key = 'changedKey222';
|
||||
}),
|
||||
null,
|
||||
null,
|
||||
[
|
||||
['object', 'key1'],
|
||||
['object', 'key2'],
|
||||
],
|
||||
],
|
||||
// 更改多个字段
|
||||
[
|
||||
produceNextJSON((json) => {
|
||||
json.declarations[0].key = 'string1111';
|
||||
json.declarations[1].key = 'boolean1111';
|
||||
}),
|
||||
null,
|
||||
null,
|
||||
[['string'], ['boolean']],
|
||||
],
|
||||
// 删除变量
|
||||
[
|
||||
produceNextJSON((json) => {
|
||||
json.declarations = json.declarations.slice(1);
|
||||
}),
|
||||
null,
|
||||
null,
|
||||
[['string']],
|
||||
],
|
||||
// 添加变量
|
||||
[
|
||||
produceNextJSON((json) => {
|
||||
json.declarations.push({ kind: 'String', key: 'newKey' });
|
||||
}),
|
||||
null,
|
||||
null,
|
||||
[],
|
||||
],
|
||||
])('test variable change', (_json, _before, _after, _disposeList) => {
|
||||
testScope.ast.set('test', _json);
|
||||
expect(renameInfo?.before || null).toEqual(_before);
|
||||
expect(renameInfo?.after || null).toEqual(_after);
|
||||
expect(disposeList).toEqual(_disposeList);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
import { vi, describe, test, expect } from 'vitest';
|
||||
|
||||
import { VariableEngine } from '../../src';
|
||||
import { simpleVariableList } from '../../__mocks__/variables';
|
||||
import { getContainer } from '../../__mocks__/container';
|
||||
|
||||
vi.mock('nanoid', () => {
|
||||
let mockId = 0;
|
||||
return {
|
||||
nanoid: () => 'mocked-id-' + mockId++,
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* 测试基本的变量声明场景
|
||||
*/
|
||||
describe('test Variable Engine', () => {
|
||||
const container = getContainer();
|
||||
const variableEngine = container.get(VariableEngine);
|
||||
const globalVariableTable = variableEngine.globalVariableTable;
|
||||
const testScope = variableEngine.createScope('test');
|
||||
const simpleCase = testScope.ast.set('simple case', simpleVariableList);
|
||||
|
||||
test('test variable Engine Dispose', () => {
|
||||
// unbind All will trigger @preDestroy
|
||||
container.unbindAll();
|
||||
|
||||
expect(variableEngine.chain.disposed).toBeTruthy();
|
||||
expect(testScope.disposed).toBeTruthy();
|
||||
expect(simpleCase.disposed).toBeTruthy();
|
||||
expect(globalVariableTable.variableKeys.length).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user