From 05eb8bafa82a1c09a7a53cf9af908fe41911d8ca Mon Sep 17 00:00:00 2001 From: twx1232375 Date: Mon, 12 May 2025 11:28:58 +0300 Subject: [PATCH 1/2] Added runtime tests --- ui2abc/tests-memo/annotate-tests.json | 3 +- ui2abc/tests-memo/readme.md | 2 +- ui2abc/tests-memo/test/arkts_run.ts | 14 + .../test/common/main_test_module_to_import.ts | 2 +- .../common/runtime/animation/Easing.test.ts | 70 + .../runtime/common/MarkableQueue.test.ts | 109 + .../runtime/memo/bind.test.ts} | 50 +- .../runtime/memo/changeListener.test.ts | 87 + .../common/runtime/memo/contextLocal.test.ts | 76 + .../test/common/runtime/memo/remember.test.ts | 182 ++ .../test/common/runtime/memo/repeat.test.ts | 281 +++ .../test/common/runtime/states/State.test.ts | 1803 +++++++++++++++++ .../test/common/runtime/tree/TreeNode.test.ts | 354 ++++ .../test/common/runtime/tree/TreePath.test.ts | 42 + .../test/common/test_module_to_import.ts | 2 +- ui2abc/tests-memo/test/testUtils.ts | 2 +- ui2abc/tests-memo/test/ts_run.ts | 2 +- ui2abc/tests-memo/test/ui2abc/arkts.test.ts | 80 - ui2abc/tests-memo/test/ui2abc_run.ts | 15 + .../tests-memo/tsconfig-compiler-plugin.json | 3 +- 20 files changed, 3074 insertions(+), 105 deletions(-) create mode 100644 ui2abc/tests-memo/test/common/runtime/animation/Easing.test.ts create mode 100644 ui2abc/tests-memo/test/common/runtime/common/MarkableQueue.test.ts rename ui2abc/tests-memo/test/{ui2abc/utils.ts => common/runtime/memo/bind.test.ts} (41%) create mode 100644 ui2abc/tests-memo/test/common/runtime/memo/changeListener.test.ts create mode 100644 ui2abc/tests-memo/test/common/runtime/memo/contextLocal.test.ts create mode 100644 ui2abc/tests-memo/test/common/runtime/memo/remember.test.ts create mode 100644 ui2abc/tests-memo/test/common/runtime/memo/repeat.test.ts create mode 100644 ui2abc/tests-memo/test/common/runtime/states/State.test.ts create mode 100644 ui2abc/tests-memo/test/common/runtime/tree/TreeNode.test.ts create mode 100644 ui2abc/tests-memo/test/common/runtime/tree/TreePath.test.ts delete mode 100644 ui2abc/tests-memo/test/ui2abc/arkts.test.ts diff --git a/ui2abc/tests-memo/annotate-tests.json b/ui2abc/tests-memo/annotate-tests.json index be0f91028..1afed7b7e 100644 --- a/ui2abc/tests-memo/annotate-tests.json +++ b/ui2abc/tests-memo/annotate-tests.json @@ -7,6 +7,7 @@ "./test/ts/**/*.ts", "./test/unmemoized/**/*.ts", "./test/ets/**/*.ts", - "./test/arkts_run.ts" + "./test/arkts_run.ts", + "./test/ts_run.ts" ] } \ No newline at end of file diff --git a/ui2abc/tests-memo/readme.md b/ui2abc/tests-memo/readme.md index 1f71f3171..f824e6d30 100644 --- a/ui2abc/tests-memo/readme.md +++ b/ui2abc/tests-memo/readme.md @@ -23,4 +23,4 @@ npm i npm run test:ts # 1. npm run test:arkts:compiler-plugin # 2. npm run test:arkts:memo-plugin # 3. -``` \ No newline at end of file +``` diff --git a/ui2abc/tests-memo/test/arkts_run.ts b/ui2abc/tests-memo/test/arkts_run.ts index e91ec0051..5e9faf48c 100644 --- a/ui2abc/tests-memo/test/arkts_run.ts +++ b/ui2abc/tests-memo/test/arkts_run.ts @@ -18,6 +18,17 @@ import { Language, setLanguage, setTransformPlugin, TransformPlugin } from "./te import { __ARKTEST__ as Basic } from "./common/basic.test" import { __ARKTEST__ as ArktsTests } from "./arkts/arkts_test.test" +import { __ARKTEST__ as Easing } from "./common/runtime/animation/Easing.test" +import { __ARKTEST__ as MarkableQueue } from "./common/runtime/common/MarkableQueue.test" +import { __ARKTEST__ as bind } from "./common/runtime/memo/bind.test" +import { __ARKTEST__ as changeListener } from "./common/runtime/memo/changeListener.test" +import { __ARKTEST__ as contextLocal } from "./common/runtime/memo/contextLocal.test" +import { __ARKTEST__ as remember } from "./common/runtime/memo/remember.test" +import { __ARKTEST__ as repeat } from "./common/runtime/memo/repeat.test" +import { __ARKTEST__ as State } from "./common/runtime/states/State.test" +import { __ARKTEST__ as TreeNode } from "./common/runtime/tree/TreeNode.test" +import { __ARKTEST__ as TreePath } from "./common/runtime/tree/TreePath.test" + setTransformPlugin(TransformPlugin.COMPILER_PLUGIN) setLanguage(Language.ArkTS) @@ -25,3 +36,6 @@ suite("memo functionality", () => { Array.of(Basic, ArktsTests) }) +suite("runtime functionality", () => { + Array.of(Easing, MarkableQueue, bind, changeListener, contextLocal, remember, repeat, State, TreeNode, TreePath) +}) diff --git a/ui2abc/tests-memo/test/common/main_test_module_to_import.ts b/ui2abc/tests-memo/test/common/main_test_module_to_import.ts index 7d423d699..2c4025593 100644 --- a/ui2abc/tests-memo/test/common/main_test_module_to_import.ts +++ b/ui2abc/tests-memo/test/common/main_test_module_to_import.ts @@ -25,4 +25,4 @@ export default function defaultSharedMemoFunction() { SharedLog.log.push("defaultSharedMemoFunction") } -export { SharedLog, separatedMemoFunction } \ No newline at end of file +export { SharedLog, separatedMemoFunction } diff --git a/ui2abc/tests-memo/test/common/runtime/animation/Easing.test.ts b/ui2abc/tests-memo/test/common/runtime/animation/Easing.test.ts new file mode 100644 index 000000000..97da8a81a --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/animation/Easing.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Assert, suite, test } from "@koalaui/harness" +import { Easing, EasingCurve, EasingStepJump } from "@koalaui/runtime" +import { int32, float64 } from "@koalaui/compat" + +function assertEasing(easing: EasingCurve, ...expected: int32[]) { + const last = expected.length - 1 + for (let i = 0; i <= last; i++) { + Assert.equal( + Math.round(100 * easing((i as float64) / last)) as int32, + expected[i] as int32, + `i=${i}: expected=${expected[i]} - ${i / last} => ${easing(i / last)} => ${Math.round(100 * easing(i / last))}` + ) + } +} + +suite("Easing", () => { + test("Linear", () => { assertEasing(Easing.Linear, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100) }) + test("Linear.inverted", () => { assertEasing(Easing.inverted(Easing.Linear), 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100) }) + test("Linear.reversed", () => { assertEasing(Easing.reversed(Easing.Linear), 100, 90, 80, 70, 60, 50, 40, 30, 20, 10, 0) }) + test("Linear.restarted", () => { assertEasing(Easing.restarted(Easing.Linear), 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 0) }) + test("Linear.repeated twice", () => { assertEasing(Easing.repeated(Easing.Linear, 2), 0, 20, 40, 60, 80, 0, 20, 40, 60, 80, 100) }) + test("Linear.repeated 4 times", () => { assertEasing(Easing.repeated(Easing.Linear, 4), 0, 40, 80, 20, 60, 0, 40, 80, 20, 60, 100) }) + test("Linear.joined with Linear.reversed", () => { assertEasing(Easing.joined(Easing.Linear, Easing.reversed(Easing.Linear)), 0, 20, 40, 60, 80, 100, 80, 60, 40, 20, 0) }) + test("Linear.thereAndBackAgain (as above)", () => { assertEasing(Easing.thereAndBackAgain(Easing.Linear), 0, 20, 40, 60, 80, 100, 80, 60, 40, 20, 0) }) + test("EaseInSine", () => { assertEasing(Easing.EaseInSine, 0, 1, 5, 11, 19, 29, 41, 55, 69, 84, 100) }) + test("EaseInSine.inverted", () => { assertEasing(Easing.inverted(Easing.EaseInSine), 0, 16, 31, 45, 59, 71, 81, 89, 95, 99, 100) }) + test("EaseInSine.reversed", () => { assertEasing(Easing.reversed(Easing.EaseInSine), 100, 84, 69, 55, 41, 29, 19, 11, 5, 1, 0) }) + test("EaseOutSine", () => { assertEasing(Easing.EaseOutSine, 0, 16, 31, 45, 59, 71, 81, 89, 95, 99, 100) }) + test("EaseOutSine.inverted", () => { assertEasing(Easing.inverted(Easing.EaseOutSine), 0, 1, 5, 11, 19, 29, 41, 55, 69, 84, 100) }) + test("EaseOutSine.reversed", () => { assertEasing(Easing.reversed(Easing.EaseOutSine), 100, 99, 95, 89, 81, 71, 59, 45, 31, 16, 0) }) + test("EaseInOutSine", () => { assertEasing(Easing.EaseInOutSine, 0, 2, 10, 21, 35, 50, 65, 79, 90, 98, 100) }) + test("EaseInOutSine.inverted", () => { assertEasing(Easing.inverted(Easing.EaseInOutSine), 0, 2, 10, 21, 35, 50, 65, 79, 90, 98, 100) }) + test("EaseInOutSine.reversed", () => { assertEasing(Easing.reversed(Easing.EaseInOutSine), 100, 98, 90, 79, 65, 50, 35, 21, 10, 2, 0) }) + test("Ease", () => { assertEasing(Easing.Ease, 0, 10, 30, 51, 68, 80, 89, 94, 98, 99, 100) }) + test("Ease.inverted", () => { assertEasing(Easing.inverted(Easing.Ease), 0, 1, 2, 6, 11, 20, 32, 49, 70, 90, 100) }) + test("Ease.reversed", () => { assertEasing(Easing.reversed(Easing.Ease), 100, 99, 98, 94, 89, 80, 68, 51, 30, 10, 0) }) + test("EaseIn", () => { assertEasing(Easing.EaseIn, 0, 2, 6, 13, 21, 32, 43, 55, 69, 84, 100) }) + test("EaseIn.inverted", () => { assertEasing(Easing.inverted(Easing.EaseIn), 0, 16, 31, 45, 57, 68, 79, 87, 94, 98, 100) }) + test("EaseIn.reversed", () => { assertEasing(Easing.reversed(Easing.EaseIn), 100, 84, 69, 55, 43, 32, 21, 13, 6, 2, 0) }) + test("EaseOut", () => { assertEasing(Easing.EaseOut, 0, 16, 31, 45, 57, 69, 79, 87, 94, 98, 100) }) + test("EaseOut.inverted", () => { assertEasing(Easing.inverted(Easing.EaseOut), 0, 2, 6, 13, 21, 31, 43, 55, 69, 84, 100) }) + test("EaseOut.reversed", () => { assertEasing(Easing.reversed(Easing.EaseOut), 100, 98, 94, 87, 79, 69, 57, 45, 31, 16, 0) }) + test("EaseInOut", () => { assertEasing(Easing.EaseInOut, 0, 2, 8, 19, 33, 50, 67, 81, 92, 98, 100) }) + test("EaseInOut.inverted", () => { assertEasing(Easing.inverted(Easing.EaseInOut), 0, 2, 8, 19, 33, 50, 67, 81, 92, 98, 100) }) + test("EaseInOut.reversed", () => { assertEasing(Easing.reversed(Easing.EaseInOut), 100, 98, 92, 81, 67, 50, 33, 19, 8, 2, 0) }) + test("custom bezier with small overflow", () => { assertEasing(Easing.cubicBezier(.3, -.3, .7, 1.3), 0, -4, 3, 15, 32, 50, 68, 85, 97, 104, 100) }) + test("custom bezier with large overflow", () => { assertEasing(Easing.cubicBezier(.3, -.7, .7, 1.7), 0, -13, -9, 6, 26, 50, 74, 95, 109, 113, 100) }) + test("6 steps with EasingStepJump.None", () => { assertEasing(Easing.steps(6, EasingStepJump.None), 0, 0, 20, 20, 40, 40, 60, 60, 80, 80, 100, 100) }) + test("11 steps with EasingStepJump.None", () => { assertEasing(Easing.steps(11, EasingStepJump.None), 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100) }) + test("10 steps with EasingStepJump.Start", () => { assertEasing(Easing.steps(10, EasingStepJump.Start), 10, 20, 30, 40, 50, 60, 70, 80, 90, 100) }) + test("10 steps with EasingStepJump.End", () => { assertEasing(Easing.steps(10, EasingStepJump.End), 0, 10, 20, 30, 40, 50, 60, 70, 80, 90) }) + test("9 steps with EasingStepJump.Both", () => { assertEasing(Easing.steps(9, EasingStepJump.Both), 10, 20, 30, 40, 50, 60, 70, 80, 90) }) +}) + +export const __ARKTEST__ = "animation/Easing.test" diff --git a/ui2abc/tests-memo/test/common/runtime/common/MarkableQueue.test.ts b/ui2abc/tests-memo/test/common/runtime/common/MarkableQueue.test.ts new file mode 100644 index 000000000..035596273 --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/common/MarkableQueue.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Assert, suite, test } from "@koalaui/harness" +import { MarkableQueue, markableQueue } from "@koalaui/common" + +const collector = new Array() + +function testQueue(queue: MarkableQueue, expected: Array) { + collector.length = 0 + queue.callCallbacks() + Assert.deepEqual(collector, expected) +} + +suite("MarkableQueue tests", () => { + + test("nothing to call without marker", () => { + const queue = markableQueue() + queue.addCallback(() => { collector.push("1a") }) + queue.addCallback(() => { collector.push("1b") }) + queue.addCallback(() => { collector.push("1c") }) + testQueue(queue, Array.of()) + }) + + test("call only marked callbacks", () => { + const queue = markableQueue() + queue.addCallback(() => { collector.push("1a") }) + queue.addCallback(() => { collector.push("1b") }) + queue.addCallback(() => { collector.push("1c") }) + queue.setMarker() + queue.addCallback(() => { collector.push("2a") }) + queue.addCallback(() => { collector.push("2b") }) + queue.addCallback(() => { collector.push("2c") }) + testQueue(queue, Array.of("1a", "1b", "1c")) + testQueue(queue, Array.of()) // called only once + }) + + test("call marked callbacks to the latest marker", () => { + const queue = markableQueue() + queue.addCallback(() => { collector.push("1a") }) + queue.addCallback(() => { collector.push("1b") }) + queue.addCallback(() => { collector.push("1c") }) + queue.setMarker() + queue.addCallback(() => { collector.push("2a") }) + queue.addCallback(() => { collector.push("2b") }) + queue.addCallback(() => { collector.push("2c") }) + queue.setMarker() + queue.addCallback(() => { collector.push("3a") }) + queue.addCallback(() => { collector.push("3b") }) + queue.addCallback(() => { collector.push("3c") }) + testQueue(queue, Array.of("1a", "1b", "1c", "2a", "2b", "2c")) + testQueue(queue, Array.of()) // called only once + }) +}) + +suite("MarkableQueue reversed tests", () => { + + test("nothing to call without marker", () => { + const queue = markableQueue(true) + queue.addCallback(() => { collector.push("1a") }) + queue.addCallback(() => { collector.push("1b") }) + queue.addCallback(() => { collector.push("1c") }) + testQueue(queue, Array.of()) + }) + + test("call only marked callbacks", () => { + const queue = markableQueue(true) + queue.addCallback(() => { collector.push("1a") }) + queue.addCallback(() => { collector.push("1b") }) + queue.addCallback(() => { collector.push("1c") }) + queue.setMarker() + queue.addCallback(() => { collector.push("2a") }) + queue.addCallback(() => { collector.push("2b") }) + queue.addCallback(() => { collector.push("2c") }) + testQueue(queue, Array.of("1c", "1b", "1a")) + testQueue(queue, Array.of()) // called only once + }) + + test("call marked callbacks from the latest marker", () => { + const queue = markableQueue(true) + queue.addCallback(() => { collector.push("1a") }) + queue.addCallback(() => { collector.push("1b") }) + queue.addCallback(() => { collector.push("1c") }) + queue.setMarker() + queue.addCallback(() => { collector.push("2a") }) + queue.addCallback(() => { collector.push("2b") }) + queue.addCallback(() => { collector.push("2c") }) + queue.setMarker() + queue.addCallback(() => { collector.push("3a") }) + queue.addCallback(() => { collector.push("3b") }) + queue.addCallback(() => { collector.push("3c") }) + testQueue(queue, Array.of("2c", "2b", "2a", "1c", "1b", "1a")) + testQueue(queue, Array.of()) // called only once + }) +}) + +export const __ARKTEST__ = "common/MarkableQueue.test" \ No newline at end of file diff --git a/ui2abc/tests-memo/test/ui2abc/utils.ts b/ui2abc/tests-memo/test/common/runtime/memo/bind.test.ts similarity index 41% rename from ui2abc/tests-memo/test/ui2abc/utils.ts rename to ui2abc/tests-memo/test/common/runtime/memo/bind.test.ts index 64eb9b8cc..b638f32c1 100644 --- a/ui2abc/tests-memo/test/ui2abc/utils.ts +++ b/ui2abc/tests-memo/test/common/runtime/memo/bind.test.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Huawei Device Co., Ltd. + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -14,27 +14,41 @@ */ import { Assert, suite, test } from "@koalaui/harness" -import { asArray, int32 } from "@koalaui/common" -import { TestNode, testRoot, testTick, mutableState, GlobalStateManager, StateContext, MutableState } from "@koalaui/runtime" -import { __id, __key, __context } from "@koalaui/runtime" +import { + GlobalStateManager, + State, + TestNode, + memoBind, + testTick, +} from "@koalaui/runtime" +import { asArray } from "@koalaui/common" -export class SharedLog { - static log: Array = new Array() +const collector = new Array() + +function testExpected(root: State, ...expected: string[]) { + collector.length = 0 + testTick(root) + Assert.deepEqual(collector, asArray(expected)) + if (expected.length > 0) testExpected(root) } /** @memo */ -export function sharedMemoFunction() { - SharedLog.log.push("sharedMemoFunction") +function sample(arg: string): void { + collector.push(arg) } -export class GlobalStateHolder { - static globalState: MutableState = GlobalStateManager.instance.mutableState(0, true) -} +suite("memo tests", () => { + test("memoBind trivial test", () => { + GlobalStateManager.reset() + const root = TestNode.create( + /** @memo */ + (_) => { + /** @memo */ + const bind = memoBind(sample, "red") + bind() + }) + testExpected(root, "red") + }) +}) -export class Log { - log: Array = new Array() -} - -export function assertResultArray(actual: Array, ...expected: T[]) { - Assert.deepEqual(actual, asArray(expected)) -} +export const __ARKTEST__ = "memo/bind.test" diff --git a/ui2abc/tests-memo/test/common/runtime/memo/changeListener.test.ts b/ui2abc/tests-memo/test/common/runtime/memo/changeListener.test.ts new file mode 100644 index 000000000..ce7f362be --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/memo/changeListener.test.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Assert, suite, test } from "@koalaui/harness" +import { + GlobalStateManager, + OnChange, + RunEffect, + State, + TestNode, + mutableState, + testTick, +} from "@koalaui/runtime" + +const collector = new Array() + +function testExpected(root: State, expected?: string) { + collector.length = 0 + testTick(root, false) + Assert.equal(collector.length, 0) + GlobalStateManager.instance.callCallbacks() + if (expected === undefined) { + Assert.equal(collector.length, 0) + } else { + Assert.equal(collector.length, 1) + Assert.equal(collector[0], expected) + } +} + +export function testChange( + /** @memo */ + content: (value: string) => void, + expected?: string +) { + GlobalStateManager.reset() + const state = mutableState("first") + const root = TestNode.create( + /** @memo */ + (node) => { content(state.value) }) + testExpected(root, expected) + state.value = "f" + "i" + "r" + "s" + "t" + testExpected(root) + state.value = "second" + testExpected(root, "second") + state.value = "third" + testExpected(root, "third") +} + +suite("changeListener tests", () => { + + test("RunEffect", () => { + testChange( + /** @memo */ + (value: string): void => { + RunEffect(value, (change: string): void => { + collector.push(change) + }) + }, + "first" + ) +}) + + test("OnChange", () => { + testChange( + /** @memo */ + (value: string): void => { + OnChange(value, (change: string): void => { + collector.push(change) + }) + } + ) + }) +}) + +export const __ARKTEST__ = "memo/changeListener.test" \ No newline at end of file diff --git a/ui2abc/tests-memo/test/common/runtime/memo/contextLocal.test.ts b/ui2abc/tests-memo/test/common/runtime/memo/contextLocal.test.ts new file mode 100644 index 000000000..2781e3727 --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/memo/contextLocal.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { asArray } from "@koalaui/common" +import { Assert, suite, test } from "@koalaui/harness" +import { + State, + TestNode, + contextLocalScope, + contextLocalValue, + mutableState, + testTick, +} from "@koalaui/runtime" + +const collector = new Array() + +function testExpected(root: State, ...expected: string[]) { + collector.length = 0 + testTick(root) + Assert.deepEqual(collector, asArray(expected)) + if (expected.length > 0) testExpected(root) +} + +suite("contextLocal tests", () => { + + test("contextLocalScope ensures name is not changed", () => { + const state = mutableState("first") + const root = TestNode.create( + /** @memo */ + (node) => { + contextLocalScope(state.value, "value", + /** @memo */ + () => { + collector.push(contextLocalValue(state.value)) + }) + }) + testExpected(root, "value") + state.value = "second" + Assert.throws(() => { testTick(root) }) + }) + + test("contextLocalScope propagates state immediately", () => { + const state = mutableState("first") + const root = TestNode.create( + /** @memo */ + (node) => { + const str = state.value + collector.push("ui:" + str) + contextLocalScope("parameter", state.value, + /** @memo */ + () => { + const param = contextLocalValue("parameter") + collector.push("state:" + param) + }) + }) + testExpected(root, "ui:first", "state:first") + state.value = "second" + testExpected(root, "ui:second", "state:second") + state.value = "third" + testExpected(root, "ui:third", "state:third") + }) +}) + +export const __ARKTEST__ = "memo/contextLocal.test" diff --git a/ui2abc/tests-memo/test/common/runtime/memo/remember.test.ts b/ui2abc/tests-memo/test/common/runtime/memo/remember.test.ts new file mode 100644 index 000000000..ce84f3740 --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/memo/remember.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { asArray, int32, float64 } from "@koalaui/compat" +import { Assert, suite, test } from "@koalaui/harness" +import { + GlobalStateManager, + State, + TestNode, + memoLifecycle, + mutableState, + once, + remember, + rememberDisposable, + rememberMutableState, + testTick, +} from "@koalaui/runtime" + +const collector = new Array() + +function testExpected(root: State, ...expected: string[]) { + collector.length = 0 + testTick(root) + Assert.deepEqual(collector, asArray(expected)) + if (expected.length > 0) testExpected(root) +} + +suite("remember tests", () => { + + test("memoLifecycle runs when attaching and detaching", () => { + GlobalStateManager.reset() + const state = mutableState(0) + const root = TestNode.create( + /** @memo */ + (node) => { + if (state.value > 0) { + memoLifecycle( + (): void => { collector.push("attach") }, + (): void => { collector.push("detach") } + ) + collector.push("update") + } + }) + testExpected(root) + state.value = 1 + testExpected(root, "attach", "update") + state.value = 2 + testExpected(root, "update") + state.value = 0 + testExpected(root, "detach") + }) + + test("once runs once when attaching to composition", () => { + GlobalStateManager.reset() + const state = mutableState(0) + const root = TestNode.create( + /** @memo */ + (node) => { + once((): void => { collector.push("once" + state.value) }) + collector.push("update" + state.value) + }) + testExpected(root, "once0", "update0") + state.value = 1 + testExpected(root, "update1") + state.value = 2 + testExpected(root, "update2") + }) + + test("remember computes once", () => { + GlobalStateManager.reset() + const root = TestNode.create( + /** @memo */ + (node) => { + remember((): void => { collector.push("inner") }) + collector.push("outer") + }) + testExpected(root, "inner", "outer") + }) + + test("remember computes once even if inner state changed", () => { + GlobalStateManager.reset() + const state = mutableState(false) + const root = TestNode.create( + /** @memo */ + (node) => { + remember((): boolean => { + collector.push("inner") + return state.value // not depended // TODO: remember cannot be void + }) + collector.push("outer") + }) + testExpected(root, "inner", "outer") + state.value = true + testExpected(root) + }) + + test("remember computes once even if outer state changed", () => { + GlobalStateManager.reset() + const state = mutableState(false) + const root = TestNode.create( + /** @memo */ + (node) => { + remember((): void => { collector.push("inner") }) + state.value // depended + collector.push("outer") + }) + testExpected(root, "inner", "outer") + state.value = true + testExpected(root, "outer") + }) + + test("intrinsic remember should not conflict each other", () => { + GlobalStateManager.reset() + const state = mutableState(false) + const root = TestNode.create( + /** @memo */ + (node) => { + state.value + ? remember((): void => { collector.push("positive") }) + : remember((): void => { collector.push("negative") }) + }) + testExpected(root, "negative") + state.value = true + testExpected(root, "positive") + state.value = false + testExpected(root, "negative") + state.value = false + testExpected(root) + }) + + test("rememberDisposable created and disposed accordingly", () => { + GlobalStateManager.reset() + const state = mutableState(false) + const root = TestNode.create( + /** @memo */ + (node) => { + if (state.value) { + rememberDisposable( + (): number => collector.push("create"), + (_: float64 | undefined): void => { collector.push("dispose") }) + } + }) + testExpected(root) + state.value = true + testExpected(root, "create") + state.value = false + testExpected(root, "dispose") + state.value = false + testExpected(root) + }) + + test("rememberMutableState computes once even if initial state changed", () => { + GlobalStateManager.reset() + const global = mutableState(0) + const root = TestNode.create( + /** @memo */ + (node) => { + const local = rememberMutableState(global.value) + collector.push("global=" + global.value) + collector.push("local=" + local.value) + }) + testExpected(root, "global=0", "local=0") + global.value++ + testExpected(root, "global=1", "local=0") + global.value++ + testExpected(root, "global=2", "local=0") + }) +}) + +export const __ARKTEST__ = "memo/remember.test" diff --git a/ui2abc/tests-memo/test/common/runtime/memo/repeat.test.ts b/ui2abc/tests-memo/test/common/runtime/memo/repeat.test.ts new file mode 100644 index 000000000..06b5308d1 --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/memo/repeat.test.ts @@ -0,0 +1,281 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Assert as assert, suite, test } from "@koalaui/harness" +import { UniqueId, KoalaCallsiteKey } from "@koalaui/common" +import { + GlobalStateManager, + Repeat, + RepeatByArray, + RepeatRange, + RepeatWithKey, + State, + TestNode, + memoLifecycle, + mutableState, + testTick, +} from "@koalaui/runtime" +import { asArray, int32 } from "@koalaui/compat"; +import { key } from "../../../testUtils" + +const collector = new Array() + +class Page { + readonly id: KoalaCallsiteKey + private readonly name: string + constructor(name: string) { + this.id = key(name) + this.name = name + } + /** @memo */ + page(): void { + memoLifecycle( + (): void => { collector.push("+" + this.name) }, + (): void => { collector.push("-" + this.name) }) + } +} + +function createPages(...names: string[]): ReadonlyArray { + return asArray(names).map((name: string) => new Page(name)) +} + +function testExpected(root: State, ...expected: string[]) { + collector.length = 0 + testTick(root) + assert.deepEqual(collector, asArray(expected)) + if (expected.length > 0) testExpected(root) +} + +function testInsert( + /** @memo */ + content: (array: ReadonlyArray) => void +) { + GlobalStateManager.reset() + const state = mutableState(createPages("three")) + const root = TestNode.create( + /** @memo */ + (_) => { content(state.value) }) + // testExpected(root, "+") + // state.value = createPages("three") + testExpected(root, "+three") + state.value = createPages("one", "three") + testExpected(root, "+one") + state.value = createPages("one", "three", "five") + testExpected(root, "+five") + state.value = createPages("one", "two", "three", "four", "five") + testExpected(root, "+two", "+four") +} + +function testRemove( + /** @memo */ + content: (array: ReadonlyArray) => void +) { + GlobalStateManager.reset() + const state = mutableState(createPages("one", "two", "three", "four", "five")) + const root = TestNode.create( + /** @memo */ + (_): void => { content(state.value) }) + testExpected(root, "+one", "+two", "+three", "+four", "+five") + state.value = createPages("one", "three", "five") + testExpected(root, "-two", "-four") + state.value = createPages("three", "five") + testExpected(root, "-one") + state.value = createPages("three") + testExpected(root, "-five") + state.value = createPages() + testExpected(root, "-three") +} + +function testSwap( + /** @memo */ + content: (array: ReadonlyArray) => void +) { + GlobalStateManager.reset() + const state = mutableState(createPages("one", "two", "three", "four", "five")) + const root = TestNode.create( + /** @memo */ + (_): void => { content(state.value) }) + testExpected(root, "+one", "+two", "+three", "+four", "+five") + state.value = createPages("two", "one", "three", "four", "five") + testExpected(root, "-one", "+one") + state.value = createPages("two", "four", "one", "three", "five") + testExpected(root, "-one", "-three", "+one", "+three") +} + +suite("repeat tests", () => { + + test("Repeat", () => { + GlobalStateManager.reset() + const state = mutableState(createPages("one")) + const root = TestNode.create( + /** @memo */ + (_): void => { + Repeat(state.value.length as int32, + /** @memo */ + (index: int32): void => { + state.value[index].page() // index-based key + }) + }) + // testExpected(root) + // state.value = createPages("one") + testExpected(root, "+one") + state.value = createPages("one", "two", "three") + testExpected(root, "+two", "+three") + state.value = createPages("one", "two") + testExpected(root, "-three") + state.value = createPages("two") + testExpected(root, "-two") // because of index-based key; should be "-one" + state.value = createPages("one", "two") + testExpected(root, "+two") // because of index-based key; should be "+one" + }) + + test("RepeatWithKey.insert", () => { + testInsert( + /** @memo */ + (array): void => { + RepeatWithKey( + array.length as int32, + (index: int32): int32 => array[index].id, + /** @memo */ + (index: int32): void => { + array[index].page() + }) + }) + }) + + test("RepeatByArray.insert", () => { + testInsert( + /** @memo */ + (array): void => { + RepeatByArray( + array, + (element: Page, _: int32): int32 => { + return element.id + }, + /** @memo */ + (element: Page, _: int32): void => { + element.page() + }) + }) + }) + + test("RepeatRange.insert", () => { + testInsert( + /** @memo */ + (array): void => { + RepeatRange( + 0, + array.length as int32, + (index: int32): Page => array[index], + (element: Page, _: int32): int32 => element.id, + /** @memo */ + (element: Page, _: int32): void => { + element.page() + }) + }) + }) + + + test("RepeatWithKey.remove", () => { + testRemove( + /** @memo */ + (array): void => { + RepeatWithKey( + array.length as int32, + (index: int32): int32 => array[index].id, + /** @memo */ + (index: int32): void => { + array[index].page() + }) + }) + }) + + test("RepeatByArray.remove", () => { + testRemove( + /** @memo */ + (array): void => { + RepeatByArray( + array, + (element: Page, _: int32): int32 => element.id, + /** @memo */ + (element: Page, _: int32): void => { + element.page() + }) + }) + }) + + test("RepeatRange.remove", () => { + testRemove( + /** @memo */ + (array): void => { + RepeatRange( + 0, + array.length as int32, + (index: int32): Page => array[index], + (element: Page, _: int32): int32 => element.id, + /** @memo */ + (element: Page, _: int32): void => { + element.page() + }) + }) + }) + + + test("RepeatWithKey.swap", () => { + testSwap( + /** @memo */ + (array): void => { + RepeatWithKey( + array.length as int32, + (index: int32): int32 => array[index].id, + /** @memo */ + (index: int32): void => { + array[index].page() + }) + }) + }) + + test("RepeatByArray.swap", () => { + testSwap( + /** @memo */ + (array): void => { + RepeatByArray( + array, + (element: Page, _: int32): int32 => element.id, + /** @memo */ + (element: Page, _: int32): void => { + element.page() + }) + }) + }) + + test("RepeatRange.swap", () => { + testSwap( + /** @memo */ + (array): void => { + RepeatRange( + 0, + array.length as int32, + (index: int32): Page => array[index], + (element: Page, _: int32): int32 => element.id, + /** @memo */ + (element: Page, _: int32): void => { + element.page() + }) + }) + }) +}) + +export const __ARKTEST__ = "memo/repeat.test" diff --git a/ui2abc/tests-memo/test/common/runtime/states/State.test.ts b/ui2abc/tests-memo/test/common/runtime/states/State.test.ts new file mode 100644 index 000000000..ec1de493e --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/states/State.test.ts @@ -0,0 +1,1803 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Assert, suite, test } from "@koalaui/harness" +import { UniqueId, KoalaCallsiteKey } from "@koalaui/common" +import { IncrementalNode, State, TestNode, testUpdate, ValueTracker, StateContext } from "@koalaui/runtime" +import { createStateManager } from "@koalaui/runtime" +import { int32, float64 } from "@koalaui/compat" +import { key } from "../../../testUtils" + +function assertNode(state: State, presentation: string) { + Assert.isFalse(state.modified) // the same node + Assert.equal(state.value.toHierarchy(), presentation) +} + +function assertState(state: State, value: Value, modified: boolean = false) { + Assert.equal(state.modified, modified) + Assert.equal(state.value, value) + Assert.equal(state.modified, modified) +} + +function assertModifiedState(state: State, value: Value) { + assertState(state, value, true) +} + +function assertStringsAndCleanup(array: Array, presentation: string) { + Assert.isNotEmpty(array) + Assert.equal(array.join(" ; "), presentation) + array.splice(0, array.length) +} + +suite("State", () => { + test("initial state is not modified", () => { + let manager = createStateManager() + let state = manager.mutableState(200) + assertState(state, 200) + }) + test("unmanaged state is modified immediately", () => { + let manager = createStateManager() + manager.frozen = true + let state = manager.mutableState(200) + state.dispose() + state.value = 404 + assertModifiedState(state, 404) + // check that modified state is updated + state.value = 404 + assertState(state, 404) + }) + test("managed state is not modified immediately", () => { + let manager = createStateManager() + manager.frozen = true + let state = manager.mutableState(200) + state.value = 404 + assertState(state, 200) + }) + test("managed state is modified on first snapshot update", () => { + let manager = createStateManager() + let state = manager.mutableState(200) + state.value = 404 + Assert.equal(testUpdate(false, manager), 1) + assertModifiedState(state, 404) + }) + test("managed state is not modified on next snapshot update", () => { + let manager = createStateManager() + let state = manager.mutableState(200) + state.value = 404 + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(testUpdate(false, manager), 0) + assertState(state, 404) + }) + test("managed state is not modified if the same value is set", () => { + let manager = createStateManager() + let state = manager.mutableState(200) + for (let index = 0; index <= 200; index++) { + state.value = index + } + Assert.equal(testUpdate(false, manager), 0) + assertState(state, 200) + }) + test("unmanaged named state does not exist anymore", () => { + let manager = createStateManager() + const state = manager.namedState("named", (): float64 => 200) + Assert.equal(state, manager.stateBy("named")!) + Assert.isDefined(manager.stateBy("named")) + state.dispose() + Assert.isUndefined(manager.stateBy("named")) + state.dispose() + Assert.isUndefined(manager.stateBy("named")) + }) + test("managed named state is not modified immediately", () => { + let manager = createStateManager() + manager.frozen = true + manager.namedState("named", (): float64 => 200) + manager.stateBy("named")!.value = 404 + Assert.equal(manager.valueBy("named"), 200) + Assert.isFalse(manager.stateBy("named")?.modified) + }) + test("managed named state is modified on first snapshot update", () => { + let manager = createStateManager() + manager.namedState("named", (): float64 => 200) + manager.stateBy("named")!.value = 404 + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(manager.valueBy("named"), 404) + Assert.isTrue(manager.stateBy("named")?.modified) + }) + test("managed named state is not modified on next snapshot update", () => { + let manager = createStateManager() + manager.namedState("named", (): float64 => 200) + manager.stateBy("named")!.value = 404 + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(manager.valueBy("named"), 404) + Assert.isFalse(manager.stateBy("named")?.modified) + }) + test("managed named state is not modified if the same value is set", () => { + let manager = createStateManager() + manager.namedState("named", (): float64 => 200) + for (let index = 0; index <= 200; index++) { + manager.stateBy("named")!.value = index + } + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(manager.valueBy("named"), 200) + Assert.isFalse(manager.stateBy("named")?.modified) + }) + test("do not allow to dispose parameter state", () => { + const name = "parameter" + const manager = createStateManager() + manager.computableState((context: StateContext) => { + const scope = context.scope(0, 1) + const param = scope.param(0, 200, undefined, name, true) // can be found by name + if (scope.unchanged) { scope.cached } else { + const state = manager.stateBy(name)! + Assert.isDefined(state) + Assert.equal>(state, param) + Assert.throws(() => { state.dispose() }) + scope.recache() + } + return undefined + }).value + }) + test("managed parameter state is modified immediately", () => { + const name = "parameter" + const manager = createStateManager() + manager.computableState((context: StateContext) => { + const scope = context.scope(0, 1) + const param = scope.param(0, 200, undefined, name, true) // can be found by name + if (scope.unchanged) { scope.cached } else { + const state = manager.stateBy(name)! + Assert.isDefined(state) + Assert.equal>(state, param) + state.value = 404 + assertModifiedState(state, 404) + // check that modified state is updated + state.value = 404 + assertState(state, 404) + scope.recache() + } + return undefined + }).value + }) + test("updatable node should be recomputable state", () => { + const manager = createStateManager() + const mutableState = manager.mutableState(true) + const updatableNode = manager.updatableNode(new IncrementalNode(), (_: StateContext) => { mutableState.value }) + // updatable node needs to be recomputed after creation + Assert.isTrue(updatableNode.recomputeNeeded) + updatableNode.value + // updatable node is already computed after accessing + Assert.isFalse(updatableNode.recomputeNeeded) + Assert.equal(testUpdate(false, manager), 0) + // updatable node is already computed because nothing is changed + Assert.isFalse(updatableNode.recomputeNeeded) + mutableState.value = !mutableState.value + // updatable node does not know that the mutable state is changed + Assert.isFalse(updatableNode.recomputeNeeded) + Assert.equal(testUpdate(false, manager), 1) + // updatable node needs to be recomputed because the mutable state is changed + Assert.isTrue(updatableNode.recomputeNeeded) + }) + test("updatable node should not use StateManager.updateSnapshot()", () => { + const manager = createStateManager() + const state = manager.updatableNode(new IncrementalNode(), (_: StateContext) => { + manager.updateSnapshot() + }) + Assert.throws(() => { state.value }) + }) + test("updatable node should not use StateManager.updatableNode(...)", () => { + const manager = createStateManager() + const state = manager.updatableNode(new IncrementalNode(), (_: StateContext) => { + manager.updatableNode(new IncrementalNode(), (_: StateContext) => { }) + }) + Assert.throws(() => { state.value }) + }) + test("updatable node should not use StateManager.computableState(...)", () => { + const manager = createStateManager() + const state = manager.updatableNode(new IncrementalNode(), (_: StateContext) => { manager.updateSnapshot() }) + Assert.throws(() => { state.value }) + }) + test("computable state should be recomputable state", () => { + const manager = createStateManager() + const mutableState = manager.mutableState(true) + const computableState = manager.computableState((_: StateContext) => mutableState.value) + // computable state needs to be recomputed after creation + Assert.isTrue(computableState.recomputeNeeded) + computableState.value + // computable state is already computed after accessing + Assert.isFalse(computableState.recomputeNeeded) + Assert.equal(testUpdate(false, manager), 0) + // computable state is already computed because nothing is changed + Assert.isFalse(computableState.recomputeNeeded) + mutableState.value = !mutableState.value + // computable state does not know that the mutable state is changed + Assert.isFalse(computableState.recomputeNeeded) + Assert.equal(testUpdate(false, manager), 1) + // computable state needs to be recomputed because the mutable state is changed + Assert.isTrue(computableState.recomputeNeeded) + }) + test("computable state should not use StateManager.updateSnapshot()", () => { + const manager = createStateManager() + const state = manager.computableState((_: StateContext) => { + manager.updateSnapshot() + return undefined + }) + Assert.throws(() => { state.value }) + }) + test("computable state should not use StateManager.updatableNode(...)", () => { + const manager = createStateManager() + const state = manager.computableState((_: StateContext) => manager.updatableNode(new IncrementalNode(), (_: StateContext) => { })) + Assert.throws(() => { state.value }) + }) + test("computable state depends on managed state", () => { + let manager = createStateManager() + let name = manager.mutableState("NAME") + // create computable state that tracks computing inner scopes + let computing = new Array() + let result = manager.computableState((context: StateContext) => { + computing.push("main") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("name"), () => { + computing.push("name") + return name.value + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= NAME =>") + assertStringsAndCleanup(computing, "main ; left ; name ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + assertState(result, "<= NAME =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + name.value = "Sergey Malenkov" + Assert.equal(testUpdate(false, manager), 1) + assertModifiedState(result, "<= Sergey Malenkov =>") + assertStringsAndCleanup(computing, "main ; name") + }) + test("computable state depends on named state managed globally", () => { + let manager = createStateManager() + // create named scope managed globally and schedule its updating + manager.namedState("global", () => "NAME") + // create computable state that tracks computing inner scopes + let computing = new Array() + let result = manager.computableState((context: StateContext) => { + computing.push("main") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("name"), () => { + computing.push("name") + return context.valueBy("global") + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= NAME =>") + assertStringsAndCleanup(computing, "main ; left ; name ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + assertState(result, "<= NAME =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + manager.stateBy("global")!.value = "Sergey Malenkov" + Assert.equal(testUpdate(false, manager), 1) + assertModifiedState(result, "<= Sergey Malenkov =>") + assertStringsAndCleanup(computing, "main ; name") + }) + test("computable state depends on named state managed locally", () => { + let manager = createStateManager() + // create computable state that tracks computing inner scopes + let computing = new Array() + let result = manager.computableState((context: StateContext) => { + computing.push("main") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("name"), () => { + computing.push("name") + const state = context.namedState("local", (): float64 => 1) + manager.scheduleCallback(() => { state.value++ }) + return state.value + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= 1 =>") + assertStringsAndCleanup(computing, "main ; left ; name ; right") + // computable state is not modified + assertState(result, "<= 1 =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + Assert.equal(testUpdate(true, manager), 1) + assertModifiedState(result, "<= 2 =>") + assertStringsAndCleanup(computing, "main ; name") + // compute state on next snapshot update + Assert.equal(testUpdate(true, manager), 1) + assertModifiedState(result, "<= 3 =>") + assertStringsAndCleanup(computing, "main ; name") + }) + test("computable state depends on named state managed locally from managed inner scope", () => { + let manager = createStateManager() + let computing = new Array() + let result = manager.computableState((context: StateContext) => { + context.namedState("local", (): float64 => 1) + computing.push("main") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("name"), () => { + computing.push("name") + const state = context.stateBy("local")! + manager.scheduleCallback(() => { state.value++ }) + return context.valueBy("local") + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= 1 =>") + assertStringsAndCleanup(computing, "main ; left ; name ; right") + // computable state is not modified + assertState(result, "<= 1 =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + Assert.equal(testUpdate(true, manager), 1) + assertModifiedState(result, "<= 2 =>") + assertStringsAndCleanup(computing, "main ; name") + // compute state on next snapshot update + Assert.equal(testUpdate(true, manager), 1) + assertModifiedState(result, "<= 3 =>") + assertStringsAndCleanup(computing, "main ; name") + }) + test("computable state allows to cleanup disposed scopes", () => { + let manager = createStateManager() + let computing = new Array() + let result = manager.computableState((context: StateContext) => { + let name = context.compute(key("value"), () => { + computing.push("compute:value") + return "value" + }) + let condition = context.compute(key("condition"), () => { + computing.push("compute:condition") + return context.valueBy("condition") + }) + let value = condition + ? context.compute(key("true"), () => { + computing.push("compute:true") + return context.compute(key("true"), () => { + context.namedState("true", () => true) + computing.push("compute:inner:true") + return "true" + }, (old: string | undefined) => { + Assert.isUndefined(context.stateBy("false")) + Assert.isDefined(context.stateBy("true")) + Assert.isTrue(context.valueBy("true")) + Assert.equal(old, "true") + computing.push("cleanup:inner:true") + }) + }, (_: string | undefined) => { computing.push("cleanup:true") }) + : context.compute(key("false"), () => { + computing.push("compute:false") + return context.compute(key("false"), () => { + context.namedState("false", () => false) + computing.push("compute:inner:false") + return "false" + }, (old: string | undefined) => { + Assert.isUndefined(context.stateBy("true")) + Assert.isDefined(context.stateBy("false")) + Assert.isFalse(context.valueBy("false")) + Assert.equal(old, "false") + computing.push("cleanup:inner:false") + }) + }, (_: string | undefined) => { computing.push("cleanup:false") }) + return name + " is " + value + }) + let condition = manager.namedState("condition", () => true) + // initial computation + Assert.equal(result.value, "value is true") + assertStringsAndCleanup(computing, "compute:value ; compute:condition ; compute:true ; compute:inner:true") + // condition changed from true to false + condition.value = false + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "value is false") + assertStringsAndCleanup(computing, "compute:condition ; compute:false ; compute:inner:false ; cleanup:true ; cleanup:inner:true") + // condition changed from false to true + condition.value = true + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "value is true") + assertStringsAndCleanup(computing, "compute:condition ; compute:true ; compute:inner:true ; cleanup:false ; cleanup:inner:false") + }) + test("global named state must be created only once", () => { + let manager = createStateManager() + let created = manager.namedState("named", (): int32 => 200) + let existed = manager.namedState("named", (): int32 => { + Assert.fail() + return 200 + }) + Assert.equal(created, existed) + Assert.equal(created, manager.stateBy("named")!) + Assert.equal(existed, manager.stateBy("named")!) + }) + test("local named state must be created only once", () => { + createStateManager().computableState((context: StateContext) => { + let created = context.namedState("named", (): int32 => 200) + let existed = context.namedState("named", (): int32 => { + Assert.fail() + return 200 + }) + Assert.equal(created, existed) + Assert.equal(created, context.stateBy("named")!) + Assert.equal(existed, context.stateBy("named")!) + return undefined + }).value + }) + test("global named state must not be created when creating another one", () => { + let manager = createStateManager() + manager.namedState("named", () => { + Assert.throws(() => { + manager.namedState("unnamed", () => 200) + }) + return 200 + }) + }) + test("local named state must not be created when creating another one", () => { + createStateManager().computableState((context: StateContext) => { + context.namedState("named", (): float64 => { + Assert.throws(() => { + context.namedState("unnamed", (): float64 => 200) + }) + return 200 + }) + return undefined + }).value + }) + test("do not allow to dispose global mutable state when creating global named state", () => { + const manager = createStateManager() + const mutable = manager.mutableState("value") + manager.namedState("prohibited", () => { + Assert.throws(() => { mutable.dispose() }) + return mutable.value + }) + }) + test("do not allow to dispose global mutable state when creating local named state", () => { + const manager = createStateManager() + const mutable = manager.mutableState("value") + const state = manager.computableState((context: StateContext) => { + context.namedState("prohibited", () => { + Assert.throws(() => { mutable.dispose() }) + return mutable.value + }) + return undefined + }) + state.value + state.dispose() + }) + test("do not allow to dispose global named state when creating global named state", () => { + const manager = createStateManager() + const named = manager.namedState("state", () => "value") + manager.namedState("prohibited", () => { + Assert.equal(named, manager.stateBy("state")!) + Assert.throws(() => { named.dispose() }) + return named.value + }) + }) + test("do not allow to dispose local named state when creating local named state", () => { + const manager = createStateManager() + const state = manager.computableState((context: StateContext) => { + const named = context.namedState("state", () => "") + context.namedState("prohibited", () => { + Assert.equal(named, manager.stateBy("state")!) + Assert.throws(() => { named.dispose() }) + return named.value + }) + return undefined + }) + state.value + state.dispose() + }) + test("do not allow to dispose global mutable state when updating computable state", () => { + const manager = createStateManager() + const mutable = manager.mutableState("value") + const state = manager.computableState((_: StateContext) => { + Assert.throws(() => { mutable.dispose() }) + return undefined + }) + state.value + state.dispose() + }) + test("do not allow to dispose global named state when updating computable state", () => { + const manager = createStateManager() + const named = manager.namedState("state", () => "") + const state = manager.computableState((_: StateContext) => { + Assert.throws(() => { named.dispose() }) + return undefined + }) + state.value + state.dispose() + }) + test("do not allow to dispose local named state when updating computable state", () => { + const manager = createStateManager() + const state = manager.computableState((context: StateContext) => { + const named = context.namedState("state", () => "") + Assert.throws(() => { named.dispose() }) + return undefined + }) + state.value + state.dispose() + }) + test("do not allow to update snapshot when updating computable state", () => { + const manager = createStateManager() + const state = manager.computableState((context: StateContext) => { + Assert.equal(context, manager) + Assert.throws(() => { manager.updateSnapshot() }) + return undefined + }) + state.value + state.dispose() + }) + test("do not allow to create updatable node when updating computable state", () => { + const manager = createStateManager() + const state = manager.computableState((context: StateContext) => { + Assert.equal(context, manager) + Assert.throws(() => { manager.updatableNode(new TestNode(), (_: StateContext) => { }) }) + return undefined + }) + state.value + state.dispose() + }) + test("computable state must not compute something when creating local named state", () => { + createStateManager().computableState((context: StateContext) => { + context.namedState("name", () => { + Assert.throws(() => { + context.compute(key("compute"), () => 200) + }) + return "NAME" + }) + return context.valueBy("name") + }).value + }) + test("computable state must not depend on managed state when creating another one", () => { + let manager = createStateManager() + let mutable = manager.mutableState("NAME") + let computable = manager.computableState((context: StateContext) => { + context.namedState("name", () => mutable.value) + return context.valueBy("name") + }) + // initial computation + assertState(computable, "NAME") + // do not update result when snapshot updated + mutable.value = "Sergey Malenkov" + Assert.equal(testUpdate(false, manager), 1) + assertState(computable, "NAME") + }) + test("computable state from specific scope must be forgotten on dispose automatically", () => { + const manager = createStateManager() + const state = manager.mutableState(-1) + const selector = manager.mutableState(true) + const computable = manager.computableState((context: StateContext) => selector.value + ? context.compute(key("first"), () => context.computableState((_: StateContext): int32 => state.value), undefined, true) + : context.compute(key("second"), () => context.computableState((_: StateContext): int32 => state.value), undefined, true)) + // initial computation + const initial = computable.value + assertState(initial, -1) + // ensure that initial state is managed + state.value = -10 + assertState(initial, -1) + Assert.equal(testUpdate(false, manager), 1) + assertModifiedState(state, -10) + assertModifiedState(initial, -10) + // dispose first inner scope + Assert.strictEqual(computable.value, initial) + selector.value = false + Assert.equal(testUpdate(false, manager), 1) + Assert.notStrictEqual(computable.value, initial) + // ensure that initial state is not managed + assertState(initial, -10) + state.value = -100 + Assert.equal(testUpdate(false, manager), 1) + assertState(initial, -10) + }) + test("named state from specific scope must be forgotten on dispose automatically", () => { + const manager = createStateManager() + manager.frozen = true + const selector = manager.mutableState(true) + const computable = manager.computableState((context: StateContext) => selector.value + ? context.compute(key("first"), () => context.namedState("name", (): int32 => -1)) + : context.compute(key("second"), () => context.namedState("name", (): int32 => 1))) + // initial computation + const initial = computable.value + assertState(initial, -1) + // ensure that initial state is managed + initial.value = -10 + assertState(initial, -1) + Assert.equal(testUpdate(false, manager), 1) + assertModifiedState(initial, -10) + // dispose first inner scope + Assert.strictEqual(computable.value, initial) + selector.value = false + Assert.equal(testUpdate(false, manager), 1) + Assert.notStrictEqual(computable.value, initial) + // ensure that initial state is not managed + assertState(initial, -10) + initial.value = -100 + assertModifiedState(initial, -100) + }) + test("mutable state from specific scope must be forgotten on dispose too", () => { + const manager = createStateManager() + manager.frozen = true + const selector = manager.mutableState(true) + const computable = manager.computableState((context: StateContext) => selector.value + ? context.compute(key("first"), () => context.mutableState(-1), undefined, true) + : context.compute(key("second"), () => context.mutableState(1), undefined, true)) + // initial computation + const initial = computable.value + assertState(initial, -1) + // ensure that initial state is managed + initial.value = -10 + assertState(initial, -1) + Assert.equal(testUpdate(false, manager), 1) + assertModifiedState(initial, -10) + // dispose first inner scope + Assert.strictEqual(computable.value, initial) + selector.value = false + Assert.equal(testUpdate(false, manager), 1) + Assert.notStrictEqual(computable.value, initial) + // ensure that initial state is not managed + assertState(initial, -10) + initial.value = -100 + assertModifiedState(initial, -100) + }) + test("mutable state must not be changed during recomposition", () => { + const manager = createStateManager() + const state = manager.mutableState(0 as float64) + const increment = () => { + Assert.throws(() => { state.value++ }) + return state.value + } + const computable = manager.computableState((context: StateContext): float64 => + context.compute(key("a"), increment) + + context.compute(key("b"), increment) + + context.compute(key("c"), increment)) + Assert.equal(computable.value, 0) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(computable.value, 0) + state.value++ + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(computable.value, 3) + }) + test("allow to compute once within a leaf scope", () => { + const computable = createStateManager().computableState((context: StateContext) => + context.compute(key("leaf"), () => + context.compute(key("allowed"), (): float64 => 0, undefined, true), + undefined, true)) + Assert.equal(computable.value, 0) + }) + test("create global mutable state in local context", () => { + const manager = createStateManager() + const computable = manager.computableState((context: StateContext) => context.mutableState(0 as float64, true)) + const globalState = computable.value + Assert.equal("GlobalState=0", globalState.toString()) + globalState.value = 1 + Assert.equal(testUpdate(false, manager), 1) + Assert.equal("GlobalState,modified=1", globalState.toString()) + Assert.equal(testUpdate(false, manager), 0) + computable.dispose() + Assert.equal("GlobalState=1", globalState.toString()) + globalState.dispose() + Assert.equal("GlobalState,disposed=1", globalState.toString()) + }) + test("create local mutable state in local context within remember", () => { + const manager = createStateManager() + const computable = manager.computableState((context: StateContext) => context.compute(0, () => context.mutableState(0 as float64, false), undefined, true)) + const localState = computable.value + Assert.equal("LocalState=0", localState.toString()) + localState.value = 1 + Assert.equal(testUpdate(false, manager), 1) + Assert.equal("LocalState,modified=1", localState.toString()) + Assert.equal(testUpdate(false, manager), 0) + computable.dispose() + Assert.equal("LocalState,disposed=1", localState.toString()) + localState.dispose() + Assert.equal("LocalState,disposed=1", localState.toString()) + }) + test("create global named state in local context", () => { + const manager = createStateManager() + const computable = manager.computableState((context: StateContext) => { + const state = context.namedState("global", (): int32 => 0, true) + Assert.equal(state, manager.stateBy("global")!) + Assert.isDefined(manager.stateBy("global", true)) + Assert.isUndefined(manager.stateBy("global", false)) + return state + }) + const globalState = computable.value + Assert.equal(globalState, manager.stateBy("global")!) + Assert.equal("GlobalState(global)=0", globalState.toString()) + globalState.value = 1 + Assert.equal(testUpdate(false, manager), 1) + Assert.equal("GlobalState(global),modified=1", globalState.toString()) + Assert.equal(testUpdate(false, manager), 0) + computable.dispose() + Assert.equal("GlobalState(global)=1", globalState.toString()) + globalState.dispose() + Assert.equal("GlobalState(global),disposed=1", globalState.toString()) + }) + test("create local named state in local context", () => { + const manager = createStateManager() + const globalState = manager.namedState("global", (): float64 => Number.MAX_SAFE_INTEGER + 1) + const computable = manager.computableState((context: StateContext) => { + Assert.equal(globalState, manager.stateBy("global")!) + Assert.isDefined(manager.stateBy("global", true)) + Assert.isUndefined(manager.stateBy("global", false)) + const state = context.namedState("local", (): float64 => 0, false) + Assert.equal(state, manager.stateBy("local")!) + Assert.isUndefined(manager.stateBy("local", true)) + Assert.isDefined(manager.stateBy("local", false)) + return state + }) + const localState = computable.value + Assert.isUndefined(manager.stateBy("local")) + Assert.equal("LocalState(local)=0", localState.toString()) + localState.value = 1 + Assert.equal(testUpdate(false, manager), 1) + Assert.equal("LocalState(local),modified=1", localState.toString()) + Assert.equal(testUpdate(false, manager), 0) + computable.dispose() + Assert.equal("LocalState(local),disposed=1", localState.toString()) + localState.dispose() + Assert.equal("LocalState(local),disposed=1", localState.toString()) + }) + test("prohibit to create local mutable state in global context", () => { + Assert.throws(() => { createStateManager().mutableState(0, false) }) + }) + test("prohibit to create local named state in global context", () => { + Assert.throws(() => { createStateManager().namedState("name", (): int32 => 0, false) }) + }) + test("prohibit to create local mutable state in local context", () => { + const computable = createStateManager().computableState((context: StateContext) => context.mutableState(0, false)) + Assert.throws(() => { computable.value }) + }) + test("prohibit to compute within a leaf scope", () => { + const computable = createStateManager().computableState((context: StateContext) => + context.compute(key("leaf"), () => + context.compute(key("prohibit"), (): float64 => 0), + undefined, true)) + Assert.throws(() => { computable.value }) + }) + test("prohibit to build tree within a leaf scope", () => { + const computable = createStateManager().updatableNode(new TestNode(), (context: StateContext) => { + context.compute(key("leaf"), () => { + context.attach(key("prohibit"), () => new TestNode(), () => { }) + return undefined + }, undefined, true) + }) + Assert.throws(() => { computable.value }) + }) + test("prohibit to build tree within a computable state", () => { + const computable = createStateManager().computableState((context: StateContext) => { + context.attach(key("prohibit"), () => new TestNode(), () => { }) + return undefined + }) + Assert.throws(() => { computable.value }) + }) + test("leaf scope must not depend on managed state", () => { + const manager = createStateManager() + const state = manager.mutableState(0) + const computing = new Array() + const computable = manager.computableState((context: StateContext) => { + computing.push("computable") + return context.compute(key("name"), () => { + computing.push("name") + return state.value + }, undefined, true) + }) + // initial computation + assertState(computable, 0) + assertStringsAndCleanup(computing, "computable ; name") + // compute state only when snapshot updated + state.value = -1 + Assert.equal(testUpdate(false, manager), 1) + assertState(computable, 0) + Assert.isEmpty(computing) + }) + test("0. amount of modified states including computable states that have dependants", () => { + const manager = createStateManager() + const mutable = manager.mutableState(false) + let computableCounter = 0 + const computable = manager.computableState((_: StateContext) => { + computableCounter++ + return mutable.value + }) + let stateCounter = 0 + const state = manager.computableState((_: StateContext) => { + stateCounter++ + return computable.value + }) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(computableCounter, 0) // computable is not recomputed + Assert.equal(stateCounter, 0) // state is not recomputed + assertState(state, false) + Assert.equal(computableCounter, 1) // computable is recomputed + Assert.equal(stateCounter, 1) // state is recomputed + mutable.value = true + Assert.equal(testUpdate(false, manager), 2) + Assert.equal(computableCounter, 2) // computable is recomputed automatically + Assert.equal(stateCounter, 1) // state is not recomputed + assertModifiedState(state, true) + Assert.equal(computableCounter, 2) // computable is not recomputed + Assert.equal(stateCounter, 2) // state is recomputed by request + }) + test("1. amount of modified states including computable states that have dependants", () => { + const manager = createStateManager() + const mutable = manager.mutableState(false) + let computableCounter = 0 + const computable = manager.computableState((_: StateContext) => { + computableCounter++ + const value = mutable.value + return value && false + }) + let stateCounter = 0 + const state = manager.computableState((_: StateContext) => { + stateCounter++ + return computable.value + }) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(computableCounter, 0) // computable is not recomputed + Assert.equal(stateCounter, 0) // state is not recomputed + assertState(state, false) + Assert.equal(computableCounter, 1) // computable is recomputed + Assert.equal(stateCounter, 1) // state is recomputed + mutable.value = true + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(computableCounter, 2) // computable is recomputed automatically + Assert.equal(stateCounter, 1) // state is not recomputed + assertState(state, false) + Assert.equal(computableCounter, 2) // computable is not recomputed + Assert.equal(stateCounter, 1) // state is not recomputed because computable is not modified + }) + test("2. amount of modified states including computable states that have dependants", () => { + const manager = createStateManager() + const mutable = manager.mutableState(false) + let computableCounter = 0 + const computable = manager.computableState((_: StateContext) => { + computableCounter++ + return mutable.value + }) + let stateCounter = 0 + const state = manager.computableState((_: StateContext) => { + stateCounter++ + const value = computable.value + return value && false + }) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(computableCounter, 0) // computable is not recomputed + Assert.equal(stateCounter, 0) // state is not recomputed + assertState(state, false) + Assert.equal(computableCounter, 1) // computable is recomputed + Assert.equal(stateCounter, 1) // state is recomputed + mutable.value = true + Assert.equal(testUpdate(false, manager), 2) + Assert.equal(computableCounter, 2) // computable is recomputed automatically + Assert.equal(stateCounter, 1) // state is not recomputed + assertState(state, false) + Assert.equal(computableCounter, 2) // computable is not recomputed + Assert.equal(stateCounter, 2) // state is recomputed by request but it is not modified + }) + test("build and update simple tree", () => { + let manager = createStateManager() + let count = manager.mutableState(30) + let first = manager.mutableState("first node") + let second = manager.mutableState("second node") + let computing = new Array() + const rootNode = new TestNode() + let firstNode: TestNode + let secondNode: TestNode + let root = manager.updatableNode(rootNode, (context: StateContext) => { + computing.push("update root") + Assert.equal(context.node, rootNode) + context.compute(key("init"), () => { + computing.push("init root") + Assert.equal(context.node, rootNode) + rootNode.content = "root" + return undefined + }) + if (count.value < 40) { + context.attach(key("first"), () => { + computing.push("create first") + Assert.equal(context.node, undefined) + return firstNode = new TestNode() + }, () => { + computing.push("update first") + Assert.equal(context.node, firstNode) + context.compute(key("init"), () => { + computing.push("init first") + Assert.equal(context.node, firstNode) + firstNode.content = first.value + return undefined + }) + }, () => { + computing.push("detach&dispose first") + Assert.equal(context.node, firstNode) + }) + } + if (count.value > 20) { + context.attach(key("second"), () => { + computing.push("create second") + Assert.equal(context.node, undefined) + return secondNode = new TestNode() + }, () => { + computing.push("update second") + Assert.equal(context.node, secondNode) + context.compute(key("init"), () => { + computing.push("init second") + Assert.equal(context.node, secondNode) + secondNode.content = second.value + return undefined + }) + }, () => { + computing.push("detach&dispose second") + Assert.equal(context.node, secondNode) + }) + } + }) + assertNode(root, + "root\n" + + " first node\n" + + " second node") + + assertStringsAndCleanup(computing, "update root ; init root ; create first ; update first ; init first ; create second ; update second ; init second") + + Assert.equal(testUpdate(false, manager), 0) + assertNode(root, + "root\n" + + " first node\n" + + " second node") + + Assert.isEmpty(computing) + + count.value = 20 + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " first node") + + assertStringsAndCleanup(computing, "update root ; detach&dispose second") + + second.value = "last node" + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " first node") + + Assert.isEmpty(computing) + + count.value = 40 + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " last node") + + assertStringsAndCleanup(computing, "update root ; create second ; update second ; init second ; detach&dispose first") + + count.value = 30 + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " first node\n" + + " last node") + + assertStringsAndCleanup(computing, "update root ; create first ; update first ; init first") + + count.value = 40 + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " last node") + + assertStringsAndCleanup(computing, "update root ; detach&dispose first") + + second.value = "second node" + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " second node") + + assertStringsAndCleanup(computing, "update root ; update second ; init second") + }) + test("build and update deep tree", () => { + let manager = createStateManager() + manager.namedState("parent", () => "parent") + manager.namedState("child", () => "child") + manager.namedState("leaf", () => "leaf") + let computing = new Array() + const rootNode = new TestNode() + let parentNode: TestNode + let childNode: TestNode + let leafNode: TestNode + let root = manager.updatableNode(rootNode, (context: StateContext) => { + computing.push("update root") + Assert.equal(context.node, rootNode) + context.compute(key("init"), () => { + computing.push("init root") + Assert.equal(context.node, rootNode) + rootNode.content = "root" + return undefined + }) + if (context.valueBy("parent").length > 0) { + context.attach(key("parent"), () => { + computing.push("create parent") + Assert.equal(context.node, undefined) + return parentNode = new TestNode() + }, () => { + computing.push("update parent") + Assert.equal(context.node, parentNode) + context.compute(key("init"), () => { + computing.push("init parent") + Assert.equal(context.node, parentNode) + parentNode.content = context.valueBy("parent") + return undefined + }) + if (context.valueBy("child").length > 0) { + context.attach(key("child"), () => { + computing.push("create child") + Assert.equal(context.node, undefined) + return childNode = new TestNode() + }, () => { + computing.push("update child") + Assert.equal(context.node, childNode) + context.compute(key("init"), () => { + computing.push("init child") + Assert.equal(context.node, childNode) + childNode.content = context.valueBy("child") + return undefined + }) + if (context.valueBy("leaf").length > 0) { + context.attach(key("leaf"), () => { + computing.push("create leaf") + Assert.equal(context.node, undefined) + return leafNode = new TestNode() + }, () => { + computing.push("update leaf") + Assert.equal(context.node, leafNode) + context.compute(key("init"), () => { + computing.push("init leaf") + Assert.equal(context.node, leafNode) + leafNode.content = context.valueBy("leaf") + return undefined + }) + }, () => { + computing.push("detach&dispose leaf") + Assert.equal(context.node, leafNode) + }) + } + }, () => { + computing.push("detach&dispose child") + Assert.equal(context.node, childNode) + }) + } + }, () => { + computing.push("detach&dispose parent") + Assert.equal(context.node, parentNode) + }) + } + }) + assertNode(root, + "root\n" + + " parent\n" + + " child\n" + + " leaf") + + assertStringsAndCleanup(computing, "update root ; init root ; create parent ; update parent ; init parent ; create child ; update child ; init child ; create leaf ; update leaf ; init leaf") + + Assert.equal(testUpdate(false, manager), 0) + assertNode(root, + "root\n" + + " parent\n" + + " child\n" + + " leaf") + + Assert.isEmpty(computing) + + manager.stateBy("leaf")!.value = "leaf node" + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " parent\n" + + " child\n" + + " leaf node") + + assertStringsAndCleanup(computing, "update root ; update parent ; update child ; update leaf ; init leaf") + + manager.stateBy("parent")!.value = "" + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root") + + assertStringsAndCleanup(computing, "update root ; detach&dispose parent ; detach&dispose child ; detach&dispose leaf") + + manager.stateBy("child")!.value = "child node" + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root") + + Assert.isEmpty(computing) + + manager.stateBy("parent")!.value = "parent node" + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " parent node\n" + + " child node\n" + + " leaf node") + + assertStringsAndCleanup(computing, "update root ; create parent ; update parent ; init parent ; create child ; update child ; init child ; create leaf ; update leaf ; init leaf") + + manager.stateBy("leaf")!.value = "" + Assert.equal(testUpdate(false, manager), 1) + assertNode(root, + "root\n" + + " parent node\n" + + " child node") + + assertStringsAndCleanup(computing, "update root ; update parent ; update child ; detach&dispose leaf") + }) + test("schedule state updating to the next frame", () => { + const manager = createStateManager() + const state = manager.mutableState(0) + manager.scheduleCallback(() => { state.value = 10 }) + assertState(state, 0) + // first frame + Assert.equal(testUpdate(true, manager), 1) + assertModifiedState(state, 10) + }) + test("schedule state updating to the second next frame", () => { + const manager = createStateManager() + const state = manager.mutableState(0) + manager.scheduleCallback(() => { manager.scheduleCallback(() => { state.value = 10 }) }) + assertState(state, 0) + // first frame + Assert.equal(testUpdate(true, manager), 0) + assertState(state, 0) + // second frame + Assert.equal(testUpdate(true, manager), 1) + assertModifiedState(state, 10) + }) + test("schedule state updating to the third next frame", () => { + const manager = createStateManager() + const state = manager.mutableState(0) + manager.scheduleCallback(() => { manager.scheduleCallback(() => { manager.scheduleCallback(() => { state.value = 10 }) }) }) + assertState(state, 0) + // first frame + Assert.equal(testUpdate(true, manager), 0) + assertState(state, 0) + // second frame + Assert.equal(testUpdate(true, manager), 0) + assertState(state, 0) + // third frame + Assert.equal(testUpdate(true, manager), 1) + assertModifiedState(state, 10) + }) +}) + + +class Data { + name: string + value: float64 + + constructor(name: string, value: float64) { + this.name = name + this.value = value + } + + static equivalent(oldD: Data, newD: Data): boolean { + return oldD.name == newD.name && oldD.value == newD.value + } +} + +suite("Equivalent", () => { + const age16 = new Data("age", 16) + test("initial state is not modified", () => { + const manager = createStateManager() + const state = manager.mutableState(age16, undefined, Data.equivalent) + assertState(state, age16) + Assert.equal(testUpdate(false, manager), 0) + assertState(state, age16) + }) + test("state is modified on first snapshot update", () => { + const manager = createStateManager() + const state = manager.mutableState(age16, undefined, Data.equivalent) + assertState(state, age16) + const age64 = new Data("age", 64) + Assert.notEqual(age16, age64) + state.value = age64 + Assert.equal(testUpdate(false, manager), 1) + assertModifiedState(state, age64) + Assert.notEqual(state.value, age16) + }) + test("state is not modified on next snapshot update", () => { + const manager = createStateManager() + const state = manager.mutableState(age16, undefined, Data.equivalent) + assertState(state, age16) + const age64 = new Data("age", 64) + state.value = age64 + Assert.equal(testUpdate(false, manager), 1) + assertModifiedState(state, age64) + Assert.notEqual(state.value, age16) + Assert.equal(testUpdate(false, manager), 0) + assertState(state, age64) + }) + test("state is not modified if values are equivalent, but returns new value", () => { + const manager = createStateManager() + const state = manager.mutableState(age16, undefined, Data.equivalent) + assertState(state, age16) + const equal = new Data("age", 16) + Assert.notEqual(equal, age16) + Assert.isTrue(Data.equivalent(equal, age16)) + state.value = equal + Assert.equal(testUpdate(false, manager), 0) + assertState(state, equal) + Assert.notEqual(state.value, age16) + }) + test("frozen state is not modified if values are equivalent, but returns new value", () => { + const manager = createStateManager() + manager.frozen = true // NB! + const state = manager.mutableState(age16, undefined, Data.equivalent) + assertState(state, age16) + const equal = new Data("age", 16) + Assert.notEqual(equal, age16) + Assert.isTrue(Data.equivalent(equal, age16)) + state.value = equal + Assert.equal(testUpdate(false, manager), 0) + assertState(state, equal) + Assert.notEqual(state.value, age16) + }) + test("state is not modified if the equivalent value is set, but returns new value", () => { + const manager = createStateManager() + const state = manager.mutableState(age16, undefined, Data.equivalent) + assertState(state, age16) + let value = age16 + for (let age = 0; age <= 16; age++) { + state.value = value = new Data("age", age) + } + Assert.equal(testUpdate(false, manager), 0) + assertState(state, value) + }) + test("frozen state is not modified if the equivalent value is set, but returns new value", () => { + const manager = createStateManager() + manager.frozen = true // NB! + const state = manager.mutableState(age16, undefined, Data.equivalent) + assertState(state, age16) + let value = age16 + for (let age = 0; age <= 16; age++) { + state.value = value = new Data("age", age) + } + Assert.equal(testUpdate(false, manager), 0) + assertState(state, value) + }) +}) + +class Tracker implements ValueTracker { + private readonly onCreateFunc: (value: Value) => Value + private readonly onUpdateFunc: (value: Value) => Value + constructor(onCreate: (value: Value) => Value, onUpdate: (value: Value) => Value) { + this.onCreateFunc = onCreate + this.onUpdateFunc = onUpdate + } + onCreate(value: Value): Value { + return this.onCreateFunc(value) + } + onUpdate(value: Value): Value { + return this.onUpdateFunc(value) + } +} + +function onCreate(onCreate: (value: Value) => Value): ValueTracker { + return new Tracker(onCreate, (value: Value): Value => value) +} + +function onUpdate(onUpdate: (value: Value) => Value): ValueTracker { + return new Tracker((value: Value): Value => value, onUpdate) +} + +suite("ValueTracker", () => { + test("track state creation", () => { + const manager = createStateManager() + const state = manager.mutableState(5, undefined, undefined, onCreate((value: int32) => value * value)) + assertState(state, 25) + }) + test("disable state modification", () => { + const manager = createStateManager() + const state = manager.mutableState(5, undefined, undefined, onUpdate((value: int32): int32 => { + throw new Error("cannot set " + value) + })) + assertState(state, 5) + Assert.throws(() => { state.value = 404 }) + Assert.equal(testUpdate(false, manager), 0) + assertState(state, 5) + }) + test("disable state modification silently", () => { + const manager = createStateManager() + const state = manager.mutableState(5, undefined, undefined, onUpdate((_: int32) => 5)) + assertState(state, 5) + state.value = 404 + Assert.equal(testUpdate(false, manager), 0) + assertState(state, 5) + }) + test("disposed state ignores tracker", () => { + const manager = createStateManager() + const state = manager.mutableState(5, undefined, undefined, onUpdate((_: int32) => 5)) + assertState(state, 5) + state.value = 404 + Assert.equal(testUpdate(false, manager), 0) + assertState(state, 5) + state.dispose() + state.value = 999 + assertModifiedState(state, 999) + }) +}) + +suite("ArrayState", () => { + test("managed array state supports #at getter", () => { + const manager = createStateManager() + const array = manager.arrayState(Array.of("one", "two", "three")) + Assert.equal(array.length, 3) + Assert.equal(array.at(0), "one") + Assert.equal(array.at(1), "two") + Assert.equal(array.at(2), "three") + Assert.equal(array.at(-1), "three") + Assert.equal(array.at(-2), "two") + Assert.equal(array.at(-3), "one") + }) + test("computable state depends on managed array state", () => { + const manager = createStateManager() + const state = manager.mutableState(1) + const array = manager.arrayState(Array.of("item")) + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + const prefix = state.value + ": " + return prefix + context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= 1: item =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= 1: item =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + state.value = 2 + array.set(0, "value") + Assert.equal(testUpdate(false, manager), 2) + Assert.equal(result.value, "<= 2: value =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + state.value = 3 + array.set(0, "value") + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= 3: value =>") + assertStringsAndCleanup(computing, "outer ; center") + // compute state only when snapshot updated + state.value = 4 + array.length = 0 + Assert.equal(testUpdate(false, manager), 2) + Assert.equal(result.value, "<= 4: =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= 4: =>") + Assert.isEmpty(computing) + }) + test("computable state depends on managed array state #copyWithin", () => { + const manager = createStateManager() + const array = manager.arrayState(Array.of("one", "two", "three")) + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= one two three =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + array.copyWithin(0, 1, 2) + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= two two three =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.copyWithin(2, 0, 1) + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= two two two =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.copyWithin(1, 2, 0) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= two two two =>") + Assert.isEmpty(computing) + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= two two two =>") + Assert.isEmpty(computing) + }) + test("computable state depends on managed array state #fill", () => { + const manager = createStateManager() + const array = manager.arrayState(Array.of("one", "two", "three")) + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= one two three =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + array.fill("X", 1, 2) + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= one X three =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.fill("X") + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= X X X =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.fill("X", 1) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= X X X =>") + Assert.isEmpty(computing) + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= X X X =>") + Assert.isEmpty(computing) + }) + test("computable state depends on managed array state #pop", () => { + const manager = createStateManager() + const array = manager.arrayState(Array.of("first", "last")) + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= first last =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= first last =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + Assert.equal(array.pop(), "last") + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= first =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + Assert.equal(array.pop(), "first") + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + Assert.isUndefined(array.pop()) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= =>") + Assert.isEmpty(computing) + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= =>") + Assert.isEmpty(computing) + }) + test("computable state depends on managed array state #push", () => { + const manager = createStateManager() + const array = manager.arrayState() + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + Assert.equal(array.push(), 0) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + Assert.equal(array.push("one"), 1) + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= one =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + Assert.equal(array.push("two", "three"), 3) + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= one two three =>") + Assert.isEmpty(computing) + }) + test("computable state depends on managed array state #reverse", () => { + const manager = createStateManager() + const array = manager.arrayState(Array.of("one", "two", "three")) + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= one two three =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + array.reverse() + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= three two one =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= three two one =>") + Assert.isEmpty(computing) + }) + test("computable state depends on managed array state #shift", () => { + const manager = createStateManager() + const array = manager.arrayState(Array.of("first", "last")) + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= first last =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= first last =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + Assert.equal(array.shift(), "first") + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= last =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + Assert.equal(array.shift(), "last") + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + Assert.isUndefined(array.shift()) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= =>") + Assert.isEmpty(computing) + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= =>") + Assert.isEmpty(computing) + }) + test("computable state depends on managed array state #sort", () => { + const manager = createStateManager() + const array = manager.arrayState(Array.of("one", "two", "three")) + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= one two three =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + array.sort() + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= one three two =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + array.sort() + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= one three two =>") + Assert.isEmpty(computing) +/* TODO: [TID 00edbb] F/ets: Failed to create the collator for en (US) + // compute state only when snapshot updated + array.sort((s1: string, s2: string) => s1.length < s2.length ? -1 : s1.length > s2.length ? 1 : s1.localeCompare(s2)) + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= one two three =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= one two three =>") + Assert.isEmpty(computing) +*/ + }) + test("computable state depends on managed array state #unshift", () => { + const manager = createStateManager() + const array = manager.arrayState() + // create computable state that tracks computing inner scopes + const computing = new Array() + const result = manager.computableState((context: StateContext) => { + computing.push("outer") + return context.compute(key("left"), () => { + computing.push("left") + return "<= " + }) + context.compute(key("center"), () => { + computing.push("center") + return context.compute(key("inner"), () => { + computing.push("inner") + return array.value.join(" ") + }) + }) + context.compute(key("right"), () => { + computing.push("right") + return " =>" + }) + }) + // initial computation + assertState(result, "<= =>") + assertStringsAndCleanup(computing, "outer ; left ; center ; inner ; right") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + Assert.equal(array.unshift(), 0) + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= =>") + Assert.isEmpty(computing) + // compute state only when snapshot updated + Assert.equal(array.unshift("one"), 1) + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= one =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // compute state only when snapshot updated + Assert.equal(array.unshift("two", "three"), 3) + Assert.equal(testUpdate(false, manager), 1) + Assert.equal(result.value, "<= two three one =>") + assertStringsAndCleanup(computing, "outer ; center ; inner") + // computable state is not modified + Assert.equal(testUpdate(false, manager), 0) + Assert.equal(result.value, "<= two three one =>") + Assert.isEmpty(computing) + }) +}) + +export const __ARKTEST__ = "states/State.test" diff --git a/ui2abc/tests-memo/test/common/runtime/tree/TreeNode.test.ts b/ui2abc/tests-memo/test/common/runtime/tree/TreeNode.test.ts new file mode 100644 index 000000000..40e11ecde --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/tree/TreeNode.test.ts @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Assert, suite, test } from "@koalaui/harness" +import { TreeNode } from "@koalaui/runtime" +import { int32, uint32, float64 } from "@koalaui/compat" + +class StringNode extends TreeNode { + readonly content: string + + constructor(content: string, ...children: TreeNode[]) { + super() + this.content = content + this.appendChildren(...children) + } + + toString(): string { + return this.content + } +} + +function contentOf(node: TreeNode): string | undefined { + return (node as StringNode)?.content +} + +function assertContent(node: TreeNode, content: string) { + Assert.equal(contentOf(node), content) +} + +function assertRoot(node: TreeNode) { + Assert.isUndefined(node.parent) + Assert.equal(node.depth, 0) + Assert.equal(node.index, -1) +} + +function assertLeaf(node: TreeNode) { + Assert.equal(node.childrenCount, 0) +} + +function assertNoChildAt(parent: TreeNode, index: int32) { + Assert.isUndefined(parent.childAt(index)) +} + +function assertChildAt(parent: TreeNode, index: int32): TreeNode { + let child = parent.childAt(index) + Assert.isDefined(child) + Assert.equal(child?.parent, parent) + Assert.equal(child?.index, index) + return child! +} + +function assertToString(root: TreeNode, expected: string) { + Assert.equal(root.toHierarchy(), expected) +} + +function assertRemoveChildrenAt(root: TreeNode, index: int32, count: uint32, expected: int32) { + let childrenCount = root.childrenCount + let children = root.removeChildrenAt(index, count) + Assert.equal(children.length, expected) + Assert.equal(root.childrenCount, childrenCount - expected) + for (let i = 0; i < expected; i++) { + assertRoot(children[i]) + } +} + +function createRoot() { + return new StringNode("root", + new StringNode("first"), + new StringNode("second"), + new StringNode("third")) +} + +function createDigitsRoot() { + return new StringNode("digits", + new StringNode("0"), + new StringNode("1"), + new StringNode("2"), + new StringNode("3"), + new StringNode("4"), + new StringNode("5"), + new StringNode("6"), + new StringNode("7"), + new StringNode("8"), + new StringNode("9")) +} + +suite("TreeNode", () => { + test("single node", () => { + let node = new StringNode("node") + assertContent(node, "node") + assertRoot(node) + assertLeaf(node) + }) + test("simple root", () => { + let root = createRoot() + assertContent(root, "root") + assertRoot(root) + Assert.equal(root.childrenCount, 3) + assertNoChildAt(root, -1) + assertNoChildAt(root, 3) + let node0 = assertChildAt(root, 0) + assertContent(node0, "first") + assertLeaf(node0) + let node1 = assertChildAt(root, 1) + assertContent(node1, "second") + assertLeaf(node1) + let node2 = assertChildAt(root, 2) + assertContent(node2, "third") + assertLeaf(node2) + const children = root.children.slice() + Assert.equal(children.length, 3) + Assert.equal(children[0], node0) + Assert.equal(children[1], node1) + Assert.equal(children[2], node2) + assertToString(root, + "root\n" + + " first\n" + + " second\n" + + " third") + }) + test("iterate for each child", () => { + let root = createRoot() + let count = 0 + let children = ["first", "second", "third"] + root.forEach((node, index) => { + Assert.equal(children[count], contentOf(node)) + count++ + }) + Assert.equal(count, 3) + }) + test("iterate for each child with index", () => { + let root = createRoot() + let count = 0 + root.forEach((node, index) => { + Assert.equal(index, count) + count++ + }) + Assert.equal(count, 3) + }) + test("iterate through not every child", () => { + let root = createRoot() + let count = 0 + Assert.isFalse(root.every((node, index) => { + count++ + return contentOf(node)?.length == 5 + })) + Assert.equal(count, 2) + }) + test("iterate through every child with index", () => { + let root = createRoot() + let count = 0 + let children = ["first", "second", "third"] + Assert.isTrue(root.every((node, index) => { + Assert.equal(index, count) + count++ + return children[index as int32] === contentOf(node) + })) + Assert.equal(count, 3) + }) + test("iterate through some children", () => { + let root = createRoot() + let count = 0 + Assert.isTrue(root.some((node, index) => { + count++ + return contentOf(node)?.length == 6 + })) + Assert.equal(count, 2) + }) + test("iterate through some children with index", () => { + let root = createRoot() + let count = 0 + Assert.isTrue(root.some((node, index) => { + Assert.equal(index, count) + count++ + return index == 0 + })) + Assert.equal(count, 1) + }) + test("find child by content", () => { + let root = createRoot() + Assert.equal(root.find((node, index): TreeNode | undefined => contentOf(node) == "second" ? node : undefined), assertChildAt(root, 1)) + }) + test("find child by index", () => { + let root = createRoot() + Assert.equal(root.find((node, index): TreeNode | undefined => index == 1 ? node : undefined), assertChildAt(root, 1)) + }) + test("insert, move and remove children", () => { + let root = createRoot() + Assert.isFalse(root.removeChild(new StringNode("second"))) // non-existent node + assertToString(root, + "root\n" + + " first\n" + + " second\n" + + " third") + let second = assertChildAt(root, 1) + second.appendChildren(new StringNode("1"), new StringNode("2"), new StringNode("3")) + assertToString(root, + "root\n" + + " first\n" + + " second\n" + + " 1\n" + + " 2\n" + + " 3\n" + + " third") + second.removeFromParent() + assertRoot(second) + assertToString(second, + "second\n" + + " 1\n" + + " 2\n" + + " 3") + assertToString(root, + "root\n" + + " first\n" + + " third") + Assert.isFalse(root.removeChild(second)) // cannot remove twice + Assert.isTrue(root.insertChildAt(0, second)) + assertToString(root, + "root\n" + + " second\n" + + " 1\n" + + " 2\n" + + " 3\n" + + " first\n" + + " third") + let first = root.removeChildAt(1) + Assert.isDefined(first) + assertRoot(first!) + assertContent(first!, "first") + root.appendChild(first!) + assertToString(root, + "root\n" + + " second\n" + + " 1\n" + + " 2\n" + + " 3\n" + + " third\n" + + " first") + }) + test("insert several children at once", () => { + let root = createRoot() + let children: TreeNode[] = [new StringNode("second.1"), new StringNode("second.2"), new StringNode("second.3")] + Assert.isFalse(root.insertChildrenAt(-1, ...children)) + Assert.isFalse(root.insertChildrenAt(4, ...children)) + Assert.isTrue(root.insertChildrenAt(2, ...children)) + assertToString(root, + "root\n" + + " first\n" + + " second\n" + + " second.1\n" + + " second.2\n" + + " second.3\n" + + " third") + Assert.isTrue(root.insertChildrenAt(0, new StringNode("zero"))) + assertToString(root, + "root\n" + + " zero\n" + + " first\n" + + " second\n" + + " second.1\n" + + " second.2\n" + + " second.3\n" + + " third") + Assert.isTrue(root.insertChildrenAt(root.childrenCount, new StringNode("third.A"), new StringNode("third.B"))) + assertToString(root, + "root\n" + + " zero\n" + + " first\n" + + " second\n" + + " second.1\n" + + " second.2\n" + + " second.3\n" + + " third\n" + + " third.A\n" + + " third.B") + }) + test("remove several children at once", () => { + let root = createDigitsRoot() + assertToString(root, + "digits\n" + + " 0\n" + + " 1\n" + + " 2\n" + + " 3\n" + + " 4\n" + + " 5\n" + + " 6\n" + + " 7\n" + + " 8\n" + + " 9") + assertRemoveChildrenAt(root, -1, 1, 0) + assertRemoveChildrenAt(root, root.childrenCount, 1, 0) + // remove leading nodes + assertRemoveChildrenAt(root, 0, 1 + root.childrenCount, 0) + assertRemoveChildrenAt(root, 0, 2, 2) + assertToString(root, + "digits\n" + + " 2\n" + + " 3\n" + + " 4\n" + + " 5\n" + + " 6\n" + + " 7\n" + + " 8\n" + + " 9") + // remove trailing nodes + assertRemoveChildrenAt(root, root.childrenCount - 1, 2, 0) + assertRemoveChildrenAt(root, root.childrenCount - 2, 2, 2) + assertToString(root, + "digits\n" + + " 2\n" + + " 3\n" + + " 4\n" + + " 5\n" + + " 6\n" + + " 7") + // remove inner nodes + assertRemoveChildrenAt(root, 1, root.childrenCount, 0) + assertRemoveChildrenAt(root, 1, 4, 4) + assertToString(root, + "digits\n" + + " 2\n" + + " 7") + }) + test("remove trailing children at once", () => { + let root = createDigitsRoot() + Assert.equal(root.childrenCount, 10) + Assert.equal(root.removeChildrenAt(-1).length, 0) + Assert.equal(root.removeChildrenAt(root.childrenCount).length, 0) + Assert.equal(root.childrenCount, 10) + Assert.equal(root.removeChildrenAt(7).length, 3) + Assert.equal(root.childrenCount, 7) + Assert.equal(root.removeChildrenAt(6).length, 1) + Assert.equal(root.childrenCount, 6) + Assert.equal(root.removeChildrenAt(1).length, 5) + Assert.equal(root.childrenCount, 1) + Assert.equal(root.removeChildrenAt(0).length, 1) + Assert.equal(root.childrenCount, 0) + }) +}) + +export const __ARKTEST__ = "tree/TreeNode.test" diff --git a/ui2abc/tests-memo/test/common/runtime/tree/TreePath.test.ts b/ui2abc/tests-memo/test/common/runtime/tree/TreePath.test.ts new file mode 100644 index 000000000..7dd27a9a7 --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/tree/TreePath.test.ts @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Assert, suite, test } from "@koalaui/harness" +import { TreePath } from "@koalaui/runtime" + +suite("TreePath", () => { + + let root = new TreePath("root") + + test("root path is root", () => Assert.strictEqual(root.root, root)) + test("root path depth", () => Assert.equal(root.depth, 0)) + test("root path has undefined parent", () => Assert.strictEqual(root.parent, undefined)) + test("root path to string", () => Assert.equal(root.toString(), "/root")) + + let parent = root.child("parent") + let current = parent.child("node") + + test("tree path has root", () => Assert.strictEqual(current.root, root)) + test("tree path has parent", () => Assert.strictEqual(current.parent, parent)) + test("tree path depth", () => Assert.equal(current.depth, 2)) + test("tree path parent depth", () => Assert.equal(current.parent?.depth, 1)) + test("tree path to string", () => Assert.equal(current.toString(), "/root/parent/node")) + + let sibling = parent.child("sibling") + + test("siblings has the same parents", () => Assert.strictEqual(current.parent, sibling.parent)) +}) + +export const __ARKTEST__ = "tree/TreePath.test" diff --git a/ui2abc/tests-memo/test/common/test_module_to_import.ts b/ui2abc/tests-memo/test/common/test_module_to_import.ts index 711d7cd62..37b9981b4 100644 --- a/ui2abc/tests-memo/test/common/test_module_to_import.ts +++ b/ui2abc/tests-memo/test/common/test_module_to_import.ts @@ -20,4 +20,4 @@ export class SharedLog { /** @memo */ export function separatedMemoFunction() { SharedLog.log.push("separatedMemoFunction") -} \ No newline at end of file +} diff --git a/ui2abc/tests-memo/test/testUtils.ts b/ui2abc/tests-memo/test/testUtils.ts index 6586d7b62..26d7b81c8 100644 --- a/ui2abc/tests-memo/test/testUtils.ts +++ b/ui2abc/tests-memo/test/testUtils.ts @@ -74,4 +74,4 @@ export function key(name: string): KoalaCallsiteKey { } else { return parseInt(new UniqueId().addString(name).compute().slice(0, 10), 16) as KoalaCallsiteKey } -} \ No newline at end of file +} diff --git a/ui2abc/tests-memo/test/ts_run.ts b/ui2abc/tests-memo/test/ts_run.ts index 12aa1eed2..0552d0db0 100644 --- a/ui2abc/tests-memo/test/ts_run.ts +++ b/ui2abc/tests-memo/test/ts_run.ts @@ -15,4 +15,4 @@ import { Language, setLanguage } from "./testUtils" -setLanguage(Language.TS) \ No newline at end of file +setLanguage(Language.TS) diff --git a/ui2abc/tests-memo/test/ui2abc/arkts.test.ts b/ui2abc/tests-memo/test/ui2abc/arkts.test.ts deleted file mode 100644 index e5277197f..000000000 --- a/ui2abc/tests-memo/test/ui2abc/arkts.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Tests for specific arkts constructions - * separate file because of incompatibility with ts syntax - */ - -import { Assert, suite, test } from "@koalaui/harness" -import { asArray, int32 } from "@koalaui/common" -import { TestNode, testRoot, testTick, mutableState, GlobalStateManager, StateContext } from "@koalaui/runtime" -import { __id, __key, __context } from "@koalaui/runtime" - -import { - SharedLog, - sharedMemoFunction, - GlobalStateHolder, - Log, - assertResultArray, -} from "./utils" - -GlobalStateHolder.globalState = GlobalStateManager.instance.mutableState(0, true) -SharedLog.log = new Array() - -class Dummy {} - -/** @memo */ -function functionWithReceiver(this: Dummy, log: Array) { - log.push("function with receiver") -} - -class TestFunctionWithReceiver extends Log { - /** @memo */ - callFunctionWithReceiver(log: Array) { - let a = new Dummy() - functionWithReceiver(a, log) - a.functionWithReceiver(log) - } - - /** @memo */ - test() { - this.log.push("function with receiver call") - GlobalStateHolder.globalState.value - this.callFunctionWithReceiver(this.log) - } -} - -suite("Functions with receiver", () => { - test("Global function with receiver", () => { - const instance = new TestFunctionWithReceiver() - const root = testRoot(instance.test) - assertResultArray(instance.log, - "function with receiver call", - "function with receiver", - "function with receiver", - ) - GlobalStateHolder.globalState.value++ - testTick(root) - assertResultArray(instance.log, - "function with receiver call", - "function with receiver", - "function with receiver", - "function with receiver call", - ) - }) -}) - -export const __ARKTEST__ = "basic.test" diff --git a/ui2abc/tests-memo/test/ui2abc_run.ts b/ui2abc/tests-memo/test/ui2abc_run.ts index a5ef92de1..0f3acbcc0 100644 --- a/ui2abc/tests-memo/test/ui2abc_run.ts +++ b/ui2abc/tests-memo/test/ui2abc_run.ts @@ -17,6 +17,17 @@ import { Language, setLanguage, setTransformPlugin, TransformPlugin } from "./te import { __ARKTEST__ as Basic } from "./common/basic.test" import { __ARKTEST__ as Ui2abcTests } from "./ui2abc/ui2abc_test.test" +import { __ARKTEST__ as Easing } from "./common/runtime/animation/Easing.test" +import { __ARKTEST__ as MarkableQueue } from "./common/runtime/common/MarkableQueue.test" +import { __ARKTEST__ as bind } from "./common/runtime/memo/bind.test" +import { __ARKTEST__ as changeListener } from "./common/runtime/memo/changeListener.test" +import { __ARKTEST__ as contextLocal } from "./common/runtime/memo/contextLocal.test" +import { __ARKTEST__ as remember } from "./common/runtime/memo/remember.test" +import { __ARKTEST__ as repeat } from "./common/runtime/memo/repeat.test" +import { __ARKTEST__ as State } from "./common/runtime/states/State.test" +import { __ARKTEST__ as TreeNode } from "./common/runtime/tree/TreeNode.test" +import { __ARKTEST__ as TreePath } from "./common/runtime/tree/TreePath.test" + setTransformPlugin(TransformPlugin.MEMO_PLUGIN) setLanguage(Language.ArkTS) @@ -24,3 +35,7 @@ suite("memo functionality", () => { Array.of(Basic, Ui2abcTests) }) +suite("runtime functionality", () => { + Array.of(Easing, MarkableQueue, bind, changeListener, contextLocal, remember, repeat, State, TreeNode, TreePath) +}) + diff --git a/ui2abc/tests-memo/tsconfig-compiler-plugin.json b/ui2abc/tests-memo/tsconfig-compiler-plugin.json index ed89db113..737d79235 100644 --- a/ui2abc/tests-memo/tsconfig-compiler-plugin.json +++ b/ui2abc/tests-memo/tsconfig-compiler-plugin.json @@ -6,7 +6,8 @@ "./test/ui2abc/**/*.ts", "./test/unmemoized/**/*.ts", "./test/ets/**/*.ts", - "./test/ui2abc_run.ts" + "./test/ui2abc_run.ts", + "./test/ts_run.ts" ], "references": [ { "path": "../../incremental/compiler-plugin" }, -- Gitee From 06c8f9e560913a0b1795de7a5e64f707bb393928 Mon Sep 17 00:00:00 2001 From: twx1232375 Date: Fri, 23 May 2025 12:42:14 +0300 Subject: [PATCH 2/2] Made 'Journal' public --- incremental/runtime/src/index.ts | 4 + ui2abc/tests-memo/test/arkts_run.ts | 3 +- .../common/runtime/states/Journal.test.ts | 101 ++++++++++++++++++ ui2abc/tests-memo/test/ui2abc_run.ts | 3 +- 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 ui2abc/tests-memo/test/common/runtime/states/Journal.test.ts diff --git a/incremental/runtime/src/index.ts b/incremental/runtime/src/index.ts index 2414d94b2..92a13f38e 100644 --- a/incremental/runtime/src/index.ts +++ b/incremental/runtime/src/index.ts @@ -119,6 +119,10 @@ export { testUpdate, } from "./memo/testing" +export { + Changes, + Journal +} from "./states/Journal" export { Disposable, disposeContent, diff --git a/ui2abc/tests-memo/test/arkts_run.ts b/ui2abc/tests-memo/test/arkts_run.ts index 5e9faf48c..95d4f768a 100644 --- a/ui2abc/tests-memo/test/arkts_run.ts +++ b/ui2abc/tests-memo/test/arkts_run.ts @@ -26,6 +26,7 @@ import { __ARKTEST__ as contextLocal } from "./common/runtime/memo/contextLocal. import { __ARKTEST__ as remember } from "./common/runtime/memo/remember.test" import { __ARKTEST__ as repeat } from "./common/runtime/memo/repeat.test" import { __ARKTEST__ as State } from "./common/runtime/states/State.test" +import { __ARKTEST__ as Journal } from "./common/runtime/states/Journal.test" import { __ARKTEST__ as TreeNode } from "./common/runtime/tree/TreeNode.test" import { __ARKTEST__ as TreePath } from "./common/runtime/tree/TreePath.test" @@ -37,5 +38,5 @@ suite("memo functionality", () => { }) suite("runtime functionality", () => { - Array.of(Easing, MarkableQueue, bind, changeListener, contextLocal, remember, repeat, State, TreeNode, TreePath) + Array.of(Easing, MarkableQueue, bind, changeListener, contextLocal, remember, repeat, State, Journal, TreeNode, TreePath) }) diff --git a/ui2abc/tests-memo/test/common/runtime/states/Journal.test.ts b/ui2abc/tests-memo/test/common/runtime/states/Journal.test.ts new file mode 100644 index 000000000..799f9ef32 --- /dev/null +++ b/ui2abc/tests-memo/test/common/runtime/states/Journal.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: the real chai exports 'assert', but 'assert' is still a keyword in ArkTS +import { Assert, suite, test } from "@koalaui/harness" +import { Changes, Journal } from "@koalaui/runtime" + +function assertChange(changes: Changes | undefined, state: Object, expected: Value) { + const change = changes?.getChange(state) + Assert.isDefined(change) + Assert.equal(change?.value, expected) +} + +function assertNoChange(changes: Changes | undefined, state: Object) { + const change = changes?.getChange(state) + Assert.isUndefined(change) +} + +function assertNoChanges(journal: Journal) { + Assert.isUndefined(journal.getChanges()) +} + +suite("Journal tests", () => { + + test("new journal has no any changes", () => { + const journal = new Journal() + assertNoChanges(journal) + }) + + test("add changes to journal", () => { + const state = new Object() + const journal = new Journal() + journal.addChange(state, "value1") + journal.addChange(state, "value2") + assertChange(journal, state, "value2") + assertNoChanges(journal) + }) + + test("add marked changes to journal", () => { + const state = new Object() + const journal = new Journal() + journal.addChange(state, "value1") + journal.addChange(state, "value2") + journal.setMarker() + assertChange(journal, state, "value2") + assertChange(journal.getChanges(), state, "value2") + }) + + test("add changes to journal after marker", () => { + const state = new Object() + const journal = new Journal() + journal.addChange(state, "value1") + journal.addChange(state, "value2") + journal.setMarker() + journal.addChange(state, "value3") + journal.addChange(state, "value4") + assertChange(journal, state, "value4") + assertChange(journal.getChanges(), state, "value2") + }) + + test("remove all changes from journal", () => { + const state = new Object() + const journal = new Journal() + journal.addChange(state, "value1") + journal.addChange(state, "value2") + journal.setMarker() + journal.addChange(state, "value3") + journal.addChange(state, "value4") + journal.clear() + assertNoChange(journal, state) + assertNoChanges(journal) + }) + + test("remove marked changes from journal", () => { + const state = new Object() + const journal = new Journal() + journal.addChange(state, "value1") + journal.addChange(state, "value2") + journal.setMarker() + journal.addChange(state, "value3") + journal.addChange(state, "value4") + const changes = journal.getChanges() + Assert.isDefined(changes) + changes?.clear() + assertChange(journal, state, "value4") + assertNoChanges(journal) + }) +}) +export const __ARKTEST__ = "states/Journal.test" diff --git a/ui2abc/tests-memo/test/ui2abc_run.ts b/ui2abc/tests-memo/test/ui2abc_run.ts index 0f3acbcc0..6dfc81d67 100644 --- a/ui2abc/tests-memo/test/ui2abc_run.ts +++ b/ui2abc/tests-memo/test/ui2abc_run.ts @@ -25,6 +25,7 @@ import { __ARKTEST__ as contextLocal } from "./common/runtime/memo/contextLocal. import { __ARKTEST__ as remember } from "./common/runtime/memo/remember.test" import { __ARKTEST__ as repeat } from "./common/runtime/memo/repeat.test" import { __ARKTEST__ as State } from "./common/runtime/states/State.test" +import { __ARKTEST__ as Journal } from "./common/runtime/states/Journal.test" import { __ARKTEST__ as TreeNode } from "./common/runtime/tree/TreeNode.test" import { __ARKTEST__ as TreePath } from "./common/runtime/tree/TreePath.test" @@ -36,6 +37,6 @@ suite("memo functionality", () => { }) suite("runtime functionality", () => { - Array.of(Easing, MarkableQueue, bind, changeListener, contextLocal, remember, repeat, State, TreeNode, TreePath) + Array.of(Easing, MarkableQueue, bind, changeListener, contextLocal, remember, repeat, State, Journal, TreeNode, TreePath) }) -- Gitee