diff --git a/packages/inula-next/test/computed.test.tsx b/packages/inula-next/test/computed.test.tsx index 793a6766d589f471a3154621a905d8dabed711ce..8e229e815779521a552d8e6a5cd084a594a07778 100644 --- a/packages/inula-next/test/computed.test.tsx +++ b/packages/inula-next/test/computed.test.tsx @@ -87,6 +87,195 @@ describe('Computed Properties', () => { container.querySelector('button')!.click(); expect(resultElement.textContent).toBe('60'); }); + + it('Should correctly compute and render a derived string state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let firstName = 'John'; + let lastName = 'Doe'; + const fullName = `${firstName} ${lastName}`; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateName() { + firstName = 'Jane'; + } + + return ( +
+

{fullName}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('John Doe'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Jane Doe'); + }); + + it('Should correctly compute and render a derived number state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let price = 10; + let quantity = 2; + const total = price * quantity; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function increaseQuantity() { + quantity += 1; + } + + return ( +
+

Total: ${total}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Total: $20'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Total: $30'); + }); + + it('Should correctly compute and render a derived boolean state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let age = 17; + const isAdult = age >= 18; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function increaseAge() { + age += 1; + } + + return ( +
+

Is Adult: {isAdult ? 'Yes' : 'No'}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Is Adult: No'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Is Adult: Yes'); + }); + + it('Should correctly compute and render a derived array state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let numbers = [1, 2, 3, 4, 5]; + const evenNumbers = numbers.filter(n => n % 2 === 0); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function addNumber() { + numbers.push(6); + } + + return ( +
+

Even numbers: {evenNumbers.join(', ')}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Even numbers: 2, 4'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Even numbers: 2, 4, 6'); + }); + + it('Should correctly compute and render a derived object state', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let user = { name: 'John', age: 30 }; + const userSummary = { ...user, isAdult: user.age >= 18 }; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateAge() { + user.age = 17; + } + + return ( +
+

+ {userSummary.name} is {userSummary.isAdult ? 'an adult' : 'not an adult'} +

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('John is an adult'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('John is not an adult'); + }); + + it('Should correctly compute state based on array index', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let items = ['Apple', 'Banana', 'Cherry']; + let index = 0; + const currentItem = items[index]; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function nextItem() { + index = (index + 1) % items.length; + } + + return ( +
+

Current item: {currentItem}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Current item: Apple'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Current item: Banana'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Current item: Cherry'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Current item: Apple'); + }); }); describe('Multiple Dependencies', () => { @@ -148,6 +337,365 @@ describe('Computed Properties', () => { container.querySelectorAll('button')[1].click(); expect(resultElement.textContent).toBe('17'); }); + it('Should correctly compute and render a derived string state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let firstName = 'John'; + let lastName = 'Doe'; + let title = 'Mr.'; + const fullName = `${title} ${firstName} ${lastName}`; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateName() { + firstName = 'Jane'; + title = 'Ms.'; + } + + return ( +
+

{fullName}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Mr. John Doe'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Ms. Jane Doe'); + }); + + it('Should correctly compute and render a derived number state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let length = 5; + let width = 3; + let height = 2; + const volume = length * width * height; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateDimensions() { + length += 1; + width += 2; + } + + return ( +
+

Volume: {volume}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Volume: 30'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Volume: 60'); + }); + + it('Should correctly compute and render a derived boolean state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let age = 20; + let hasLicense = false; + let hasCar = true; + const canDrive = age >= 18 && hasLicense && hasCar; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateStatus() { + hasLicense = true; + } + + return ( +
+

Can Drive: {canDrive ? 'Yes' : 'No'}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Can Drive: No'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Can Drive: Yes'); + }); + + it('Should correctly compute and render a derived array state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let numbers1 = [1, 2, 3]; + let numbers2 = [4, 5, 6]; + let filterEven = true; + const result = [...numbers1, ...numbers2].filter(n => (filterEven ? n % 2 === 0 : n % 2 !== 0)); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function toggleFilter() { + filterEven = !filterEven; + } + + return ( +
+

Filtered numbers: {result.join(', ')}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Filtered numbers: 2, 4, 6'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Filtered numbers: 1, 3, 5'); + }); + + it('Should correctly compute and render a derived object state from multi dependency', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let user = { name: 'John', age: 30 }; + let settings = { theme: 'dark', fontSize: 14 }; + let isLoggedIn = true; + const userProfile = { + ...user, + ...settings, + status: isLoggedIn ? 'Online' : 'Offline', + }; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateStatus() { + isLoggedIn = false; + settings.theme = 'light'; + } + + return ( +
+

+ {userProfile.name} ({userProfile.age}) - {userProfile.status} - Theme: {userProfile.theme} +

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('John (30) - Online - Theme: dark'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('John (30) - Offline - Theme: light'); + }); + }); + + describe('Advanced Computed States', () => { + it('Should support basic arithmetic operations', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let a = 10; + let b = 5; + const sum = a + b; + const difference = a - b; + const product = a * b; + const quotient = a / b; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return ( +
+

Sum: {sum}

+

Difference: {difference}

+

Product: {product}

+

Quotient: {quotient}

+
+ ); + } + + render(App, container); + + expect(resultElement.innerHTML).toBe('

Sum: 15

Difference: 5

Product: 50

Quotient: 2

'); + }); + + it('Should support array indexing', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let arr = [10, 20, 30, 40, 50]; + let index = 2; + const value = arr[index]; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateIndex() { + index = 4; + } + + return ( +
+

Value: {value}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Value: 30'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Value: 50'); + }); + + it('Should support property access', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let obj = { name: 'John', age: 30 }; + const name = obj.name; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateName() { + obj.name = 'Jane'; + } + + return ( +
+

Name: {name}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Name: John'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Name: Jane'); + }); + + it('Should support function calls', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let numbers = [1, 2, 3, 4, 5]; + const sum = numbers.reduce((a, b) => a + b, 0); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return

Sum: {sum}

; + } + + render(App, container); + + expect(resultElement.textContent).toBe('Sum: 15'); + }); + + it('Should support various number operations', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let num = 3.14159; + const rounded = Math.round(num); + const floored = Math.floor(num); + const ceiled = Math.ceil(num); + const squared = Math.pow(num, 2); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return ( +
+

Rounded: {rounded}

+

Floored: {floored}

+

Ceiled: {ceiled}

+

Squared: {squared.toFixed(2)}

+
+ ); + } + + render(App, container); + + expect(resultElement.innerHTML).toBe('

Rounded: 3

Floored: 3

Ceiled: 4

Squared: 9.87

'); + }); + + it('Should support map operations', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let numbers = [1, 2, 3, 4, 5]; + const squaredNumbers = numbers.map(n => n * n); + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + return

Squared: {squaredNumbers.join(', ')}

; + } + + render(App, container); + + expect(resultElement.textContent).toBe('Squared: 1, 4, 9, 16, 25'); + }); + + it('Should support conditional expressions', ({ container }) => { + let resultElement: HTMLElement; + + function App() { + let age = 20; + let hasLicense = true; + const canDrive = age >= 18 ? (hasLicense ? 'Yes' : 'No, needs license') : 'No, too young'; + + didMount(() => { + resultElement = container.querySelector('[data-testid="result"]')!; + }); + + function updateAge() { + age = 16; + } + + return ( +
+

Can Drive: {canDrive}

+ +
+ ); + } + + render(App, container); + + expect(resultElement.textContent).toBe('Can Drive: Yes'); + container.querySelector('button')!.click(); + expect(resultElement.textContent).toBe('Can Drive: No, too young'); + }); }); describe('Nested Computed Properties', () => { diff --git a/packages/inula-next/test/state.test.tsx b/packages/inula-next/test/state.test.tsx index 4bcf9bf67e6d77c34ee7aeca5a7311474aad35db..d5156f38281cd97f66bcf796d3baaf8ab38d0824 100644 --- a/packages/inula-next/test/state.test.tsx +++ b/packages/inula-next/test/state.test.tsx @@ -25,20 +25,351 @@ vi.mock('../src/scheduler', async () => { }; }); -describe('state', () => { - it('should support "++"', ({ container }) => { - let incrementCount: () => void; - function UserInput() { +describe('Declare state', () => { + it('Should correctly declare and render a string state variable', ({ container }) => { + function StringState() { + let str = 'Hello, World!'; + return

{str}

; + } + render(StringState, container); + expect(container.innerHTML).toBe('

Hello, World!

'); + }); + + it('Should correctly declare and render a number state variable', ({ container }) => { + function NumberState() { + let num = 42; + return

{num}

; + } + render(NumberState, container); + expect(container.innerHTML).toBe('

42

'); + }); + + it('Should correctly declare and render a boolean state variable', ({ container }) => { + function BooleanState() { + let bool = true; + return

{bool.toString()}

; + } + render(BooleanState, container); + expect(container.innerHTML).toBe('

true

'); + }); + + it('Should correctly declare and render an array state variable', ({ container }) => { + function ArrayState() { + let arr = [1, 2, 3]; + return

{arr.join(', ')}

; + } + render(ArrayState, container); + expect(container.innerHTML).toBe('

1, 2, 3

'); + }); + + it('Should correctly declare and render an object state variable', ({ container }) => { + function ObjectState() { + let obj = { name: 'John', age: 30 }; + return

{`${obj.name}, ${obj.age}`}

; + } + render(ObjectState, container); + expect(container.innerHTML).toBe('

John, 30

'); + }); + + it('Should correctly declare and render a map state variable', ({ container }) => { + function MapState() { + let map = new Map([ + ['key1', 'value1'], + ['key2', 'value2'], + ]); + return ( +

+ {Array.from(map) + .map(([k, v]) => `${k}:${v}`) + .join(', ')} +

+ ); + } + render(MapState, container); + expect(container.innerHTML).toBe('

key1:value1, key2:value2

'); + }); + + it('Should correctly declare and render a set state variable', ({ container }) => { + function SetState() { + let set = new Set([1, 2, 3]); + return

{Array.from(set).join(', ')}

; + } + render(SetState, container); + expect(container.innerHTML).toBe('

1, 2, 3

'); + }); +}); + +describe('Update state', () => { + it('Should correctly update and render a string state variable', ({ container }) => { + let updateStr: () => void; + function StringState() { + let str = 'Hello'; + updateStr = () => { + str = 'Hello, World!'; + }; + return

{str}

; + } + render(StringState, container); + expect(container.innerHTML).toBe('

Hello

'); + updateStr(); + expect(container.innerHTML).toBe('

Hello, World!

'); + }); + + it('Should correctly update and render a number state variable', ({ container }) => { + let updateNum: () => void; + function NumberState() { + let num = 42; + updateNum = () => { + num = 84; + }; + return

{num}

; + } + render(NumberState, container); + expect(container.innerHTML).toBe('

42

'); + updateNum(); + expect(container.innerHTML).toBe('

84

'); + }); + + it('Should correctly update and render a boolean state variable', ({ container }) => { + let toggleBool: () => void; + function BooleanState() { + let bool = true; + toggleBool = () => { + bool = !bool; + }; + return

{bool.toString()}

; + } + render(BooleanState, container); + expect(container.innerHTML).toBe('

true

'); + toggleBool(); + expect(container.innerHTML).toBe('

false

'); + }); + + it('Should correctly update and render an object state variable', ({ container }) => { + let updateObj: () => void; + function ObjectState() { + let obj = { name: 'John', age: 30 }; + updateObj = () => { + obj.age = 31; + }; + return

{`${obj.name}, ${obj.age}`}

; + } + render(ObjectState, container); + expect(container.innerHTML).toBe('

John, 30

'); + updateObj(); + expect(container.innerHTML).toBe('

John, 31

'); + }); + + it('Should correctly handle increment operations (n++)', ({ container }) => { + let increment: () => void; + function IncrementState() { let count = 0; - incrementCount = () => { + increment = () => { count++; }; + return

{count}

; + } + render(IncrementState, container); + expect(container.innerHTML).toBe('

0

'); + increment(); + expect(container.innerHTML).toBe('

1

'); + }); - return

{count}

; + it('Should correctly handle decrement operations (n--)', ({ container }) => { + let decrement: () => void; + function DecrementState() { + let count = 5; + decrement = () => { + count--; + }; + return

{count}

; + } + render(DecrementState, container); + expect(container.innerHTML).toBe('

5

'); + decrement(); + expect(container.innerHTML).toBe('

4

'); + }); + + it('Should correctly handle operations (+=)', ({ container }) => { + let addValue: (value: number) => void; + function AdditionAssignmentState() { + let count = 10; + addValue = (value: number) => { + count += value; + }; + return

{count}

; + } + render(AdditionAssignmentState, container); + expect(container.innerHTML).toBe('

10

'); + addValue(5); + expect(container.innerHTML).toBe('

15

'); + addValue(-3); + expect(container.innerHTML).toBe('

12

'); + }); + + it('Should correctly handle operations (-=)', ({ container }) => { + let subtractValue: (value: number) => void; + function SubtractionAssignmentState() { + let count = 20; + subtractValue = (value: number) => { + count -= value; + }; + return

{count}

; + } + render(SubtractionAssignmentState, container); + expect(container.innerHTML).toBe('

20

'); + subtractValue(7); + expect(container.innerHTML).toBe('

13

'); + subtractValue(-4); + expect(container.innerHTML).toBe('

17

'); + }); + + it('Should correctly update and render a state variable as an index of array', ({ container }) => { + let updateIndex: () => void; + function ArrayIndexState() { + const items = ['Apple', 'Banana', 'Cherry', 'Date']; + let index = 0; + updateIndex = () => { + index = (index + 1) % items.length; + }; + return

{items[index]}

; + } + render(ArrayIndexState, container); + expect(container.innerHTML).toBe('

Apple

'); + updateIndex(); + expect(container.innerHTML).toBe('

Banana

'); + updateIndex(); + expect(container.innerHTML).toBe('

Cherry

'); + updateIndex(); + expect(container.innerHTML).toBe('

Date

'); + updateIndex(); + expect(container.innerHTML).toBe('

Apple

'); + }); + + it('Should correctly update and render a state variable as a property of an object', ({ container }) => { + let updatePerson: () => void; + function ObjectPropertyState() { + let person = { name: 'Alice', age: 30, job: 'Engineer' }; + updatePerson = () => { + person.age += 1; + person.job = 'Senior Engineer'; + }; + return ( +
+

Name: {person.name}

+

Age: {person.age}

+

Job: {person.job}

+
+ ); + } + render(ObjectPropertyState, container); + expect(container.innerHTML).toBe('

Name: Alice

Age: 30

Job: Engineer

'); + updatePerson(); + expect(container.innerHTML).toBe('

Name: Alice

Age: 31

Job: Senior Engineer

'); + }); + + it('Should correctly update and render an array state variable - push operation', ({ container }) => { + let pushItem: () => void; + function ArrayPushState() { + let items = ['Apple', 'Banana']; + pushItem = () => { + items.push('Cherry'); + }; + return

{items.join(', ')}

; + } + render(ArrayPushState, container); + expect(container.innerHTML).toBe('

Apple, Banana

'); + pushItem(); + expect(container.innerHTML).toBe('

Apple, Banana, Cherry

'); + }); + + it('Should correctly update and render an array state variable - pop operation', ({ container }) => { + let popItem: () => void; + function ArrayPopState() { + let items = ['Apple', 'Banana', 'Cherry']; + popItem = () => { + items.pop(); + }; + return

{items.join(', ')}

; + } + render(ArrayPopState, container); + expect(container.innerHTML).toBe('

Apple, Banana, Cherry

'); + popItem(); + expect(container.innerHTML).toBe('

Apple, Banana

'); + }); + + it('Should correctly update and render an array state variable - unshift operation', ({ container }) => { + let unshiftItem: () => void; + function ArrayUnshiftState() { + let items = ['Banana', 'Cherry']; + unshiftItem = () => { + items.unshift('Apple'); + }; + return

{items.join(', ')}

; + } + render(ArrayUnshiftState, container); + expect(container.innerHTML).toBe('

Banana, Cherry

'); + unshiftItem(); + expect(container.innerHTML).toBe('

Apple, Banana, Cherry

'); + }); + + it('Should correctly update and render an array state variable - shift operation', ({ container }) => { + let shiftItem: () => void; + function ArrayShiftState() { + let items = ['Apple', 'Banana', 'Cherry']; + shiftItem = () => { + items.shift(); + }; + return

{items.join(', ')}

; + } + render(ArrayShiftState, container); + expect(container.innerHTML).toBe('

Apple, Banana, Cherry

'); + shiftItem(); + expect(container.innerHTML).toBe('

Banana, Cherry

'); + }); + + it('Should correctly update and render an array state variable - splice operation', ({ container }) => { + let spliceItems: () => void; + function ArraySpliceState() { + let items = ['Apple', 'Banana', 'Cherry', 'Date']; + spliceItems = () => { + items.splice(1, 2, 'Elderberry', 'Fig'); + }; + return

{items.join(', ')}

; + } + render(ArraySpliceState, container); + expect(container.innerHTML).toBe('

Apple, Banana, Cherry, Date

'); + spliceItems(); + expect(container.innerHTML).toBe('

Apple, Elderberry, Fig, Date

'); + }); + + it('Should correctly update and render an array state variable - filter operation', ({ container }) => { + let filterItems: () => void; + function ArrayFilterState() { + let items = ['Apple', 'Banana', 'Cherry', 'Date']; + filterItems = () => { + items = items.filter(item => item.length > 5); + }; + return

{items.join(', ')}

; + } + render(ArrayFilterState, container); + expect(container.innerHTML).toBe('

Apple, Banana, Cherry, Date

'); + filterItems(); + expect(container.innerHTML).toBe('

Banana, Cherry

'); + }); + + it('Should correctly update and render an array state variable - map operation', ({ container }) => { + let mapItems: () => void; + function ArrayMapState() { + let items = ['apple', 'banana', 'cherry']; + mapItems = () => { + items = items.map(item => item.toUpperCase()); + }; + return

{items.join(', ')}

; } - render(UserInput, container); - expect(container.innerHTML).toBe('

0

'); - incrementCount(); - expect(container.innerHTML).toBe('

1

'); + render(ArrayMapState, container); + expect(container.innerHTML).toBe('

apple, banana, cherry

'); + mapItems(); + expect(container.innerHTML).toBe('

APPLE, BANANA, CHERRY

'); }); }); diff --git a/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts b/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts index 92d76574b917948d3b88e2ab972875e56911efe6..2918694cf701c534a80d379c0b033ad475d74fd9 100644 --- a/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts +++ b/packages/transpiler/babel-inula-next-core/src/analyze/utils.ts @@ -58,7 +58,11 @@ export function extractFnBody(node: t.FunctionExpression | t.ArrowFunctionExpres } export function isStaticValue(node: t.VariableDeclarator['init']) { - return t.isLiteral(node) || t.isArrowFunctionExpression(node) || t.isFunctionExpression(node); + return ( + (t.isLiteral(node) && !t.isTemplateLiteral(node)) || + t.isArrowFunctionExpression(node) || + t.isFunctionExpression(node) + ); } export function assertComponentNode(node: any): asserts node is ComponentNode {