diff --git a/arkoala-arkts/arkui/config/etsconfig-base.json b/arkoala-arkts/arkui/config/etsconfig-base.json index a1bc1c4e3cb49a2235a702a6b4e3f4f7125c59a9..e8d063d6165c8bcde931cb95c551b544e90af245 100644 --- a/arkoala-arkts/arkui/config/etsconfig-base.json +++ b/arkoala-arkts/arkui/config/etsconfig-base.json @@ -1,5 +1,6 @@ { "files": [ + "../../../arkoala-arkts/arkui/src/index-test.d.ts", "../../../arkoala-arkts/arkui/types/index-full.d.ts", "../../../incremental/tools/panda/arkts/std-lib/global.d.ts" ], @@ -225,7 +226,8 @@ "UIExtensionComponent", "RichEditor", "CachedImage", - "MediaCachedImage" + "MediaCachedImage", + "TestComponent" ], "extend": { "decorator": [ @@ -802,6 +804,11 @@ "name": "MediaCachedImage", "type": "MediaCachedImageAttribute", "instance": "MediaCachedImageInstance" + }, + { + "name": "TestComponent", + "type": "TestComponentAttribute", + "instance": "TestComponentInstance" } ] }, diff --git a/arkoala-arkts/arkui/src/ArkTestComponent.ts b/arkoala-arkts/arkui/src/ArkTestComponent.ts new file mode 100644 index 0000000000000000000000000000000000000000..ecf7628affbb6eac506a807ff440eb6a138817d2 --- /dev/null +++ b/arkoala-arkts/arkui/src/ArkTestComponent.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024-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 { NodeAttach, remember } from "@koalaui/runtime" +import { ArkCommonMethodComponent } from "./generated/ArkCommon" +import { ArkTestComponentPeer } from "./peers/ArkTestComponentPeer" +import { CommonMethod } from "./generated/ArkCommonInterfaces" +import { InteropNativeModule } from "@koalaui/interop" + +/** @memo:stable */ +export class ArkTestComponentComponent extends ArkCommonMethodComponent { + getPeer(): ArkTestComponentPeer { + return (this.peer as ArkTestComponentPeer) + } + /** @memo */ + setTestComponentOptions(options?: TestComponentOptions): this { + if (this.checkPriority("setColumnOptions")) { + const options_casted = options as (TestComponentOptions | undefined) + this.getPeer()?.setTestComponentOptionsAttribute(options_casted) + return this + } + return this + } + /** @memo */ + onChange(value: Function0): this { + if (this.checkPriority("onChange")) { + this.getPeer()?.onChangeAttribute(value) + return this + } + return this + } + /** @memo */ + log(message: string): this { + if (this.checkPriority("log")) { + this.getPeer()?.logAttribute(message) + return this + } + return this + } + public applyAttributesFinish(): void { + // we calls this function outside of class, so need to make it public + super.applyAttributesFinish() + } +} + +/** @memo */ +export function ArkTestComponent( + /** @memo */ + style: ((attributes: ArkTestComponentComponent) => void) | undefined, + /** @memo */ + content_: (() => void) | undefined, + options?: TestComponentOptions | undefined +) { + const receiver = remember(() => { + return new ArkTestComponentComponent() + }) + NodeAttach((): ArkTestComponentPeer => ArkTestComponentPeer.create(receiver), (_: ArkTestComponentPeer) => { + receiver.setTestComponentOptions(options) + style?.(receiver) + content_?.() + receiver.applyAttributesFinish() + }) +} + +export interface TestComponentOptions { + id?: number; +} +export type TestComponentInterface = (options?: TestComponentOptions) => TestComponentAttribute; +export interface TestComponentAttribute extends CommonMethod { + onChange?: Function0; + log?: string +} diff --git a/arkoala-arkts/arkui/src/generated/PeerNode.ts b/arkoala-arkts/arkui/src/generated/PeerNode.ts index d39f4d705078b83b4353c2d3787b3e52ac394aa3..7a30ef6f0d1257a1ed39804312d5ceab4e299b34 100644 --- a/arkoala-arkts/arkui/src/generated/PeerNode.ts +++ b/arkoala-arkts/arkui/src/generated/PeerNode.ts @@ -16,6 +16,16 @@ export class PeerNode extends IncrementalNode { static nextId(): int32 { return ++PeerNode.currentId } private id: int32 + setId(id: int32) { + PeerNode.peerNodeMap.delete(this.id) + this.id = id + PeerNode.peerNodeMap.set(this.id, this) + } + + getId(): int32 { + return this.id + } + private static peerNodeMap = new Map() static findPeerByNativeId(id: number): PeerNode | undefined { diff --git a/arkoala-arkts/arkui/src/generated/index.ts b/arkoala-arkts/arkui/src/generated/index.ts index 6607c0e112515c370fb79609d242ea633b1d3bf5..ce94405345c0e8634db0e6d9f5df44a82c2ba95c 100644 --- a/arkoala-arkts/arkui/src/generated/index.ts +++ b/arkoala-arkts/arkui/src/generated/index.ts @@ -416,3 +416,6 @@ export * from "./ArkCustomSpanMaterialized" export * from "./ArkLinearIndicatorControllerMaterialized" export * from "./ArkGlobalScopeInspectorMaterialized" export * from "./GlobalScope" +export * from "./Events" +export * from "./PeerNode" +export * from "./peers/CallbacksChecker" diff --git a/arkoala-arkts/arkui/src/index-test.d.ts b/arkoala-arkts/arkui/src/index-test.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..1a192fbe69370f380ba3bf00b0f667780ca7ba56 --- /dev/null +++ b/arkoala-arkts/arkui/src/index-test.d.ts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024-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. + */ + +declare interface TestComponentOptions { + id?: (number); +} +declare interface TestComponentInterface { + (options?: TestComponentOptions): TestComponentAttribute; +} +declare class TestComponentAttribute extends CommonMethod { + onChange(callback: () => void): TestComponentAttribute; + log(message: string): TestComponentAttribute; +} +declare const TestComponent: TestComponentInterface +declare const TestComponentInstance: TestComponentAttribute diff --git a/arkoala-arkts/arkui/src/index.ts b/arkoala-arkts/arkui/src/index.ts index b8ebda64d9c0bea28ee0443322ce9c17244da51b..2945b8f2492f7c85e4c7e21a6c051daea51935c0 100644 --- a/arkoala-arkts/arkui/src/index.ts +++ b/arkoala-arkts/arkui/src/index.ts @@ -25,4 +25,6 @@ export * from "./stateOf" export * from "./ForEach" export * from "./LazyForEach" export * from "./ohos.router" -export * from "./ArkNavigation" \ No newline at end of file +export * from "./ArkNavigation" +export * from "./peers/ArkTestComponentPeer" +export * from "./ArkTestComponent" \ No newline at end of file diff --git a/arkoala-arkts/arkui/src/peers/ArkTestComponentPeer.ts b/arkoala-arkts/arkui/src/peers/ArkTestComponentPeer.ts new file mode 100644 index 0000000000000000000000000000000000000000..431e97dff35b652f3edd28789c7205917037a342 --- /dev/null +++ b/arkoala-arkts/arkui/src/peers/ArkTestComponentPeer.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024-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 { int32 } from "@koalaui/common" +import { InteropNativeModule, KPointer } from "@koalaui/interop" +import { ComponentBase } from "../generated/ComponentBase" +import { ArkUIGeneratedNativeModule } from "#components" +import { ArkCommonMethodPeer, ArkCommonMethodAttributes } from "../generated/peers/ArkCommonPeer" +import { PeerNode } from "../generated/PeerNode" +import { TestComponentOptions } from "../ArkTestComponent" +import { UserView } from "../UserView" + +export class ArkTestComponentPeer extends ArkCommonMethodPeer { + protected constructor(peerPtr: KPointer, id: int32, name: string = "", flags: int32 = 0) { + super(peerPtr, id, name, flags) + } + public static create(component?: ComponentBase, flags: int32 = 0): ArkTestComponentPeer { + const peerId = PeerNode.nextId() + const _peerPtr = ArkUIGeneratedNativeModule._Blank_construct(peerId, flags) + const _peer = new ArkTestComponentPeer(_peerPtr, peerId, "Blank", flags) + component?.setPeer(_peer) + return _peer + } + setTestComponentOptionsAttribute(option?: TestComponentOptions): void { + if (option !== undefined) { + this.setId(option!.id! as int32) + } + } + onChangeCallback: Function0 | undefined = undefined + onChangeAttribute(callback: Function0): void { + this.onChangeCallback = callback + } + logAttribute(message: string): void { + InteropNativeModule._AppendGroupedLog(1, message) + } +} +export interface ArkTestComponentAttributes extends ArkCommonMethodAttributes { + onChange?: Function0 +} diff --git a/arkoala-arkts/arkui/tsconfig.json b/arkoala-arkts/arkui/tsconfig.json index dde0d9dc14e685050f3682f5b16db39add8b69a3..341b6914c6c308b55c3d6af81d0cab33420e630d 100644 --- a/arkoala-arkts/arkui/tsconfig.json +++ b/arkoala-arkts/arkui/tsconfig.json @@ -24,6 +24,7 @@ "../../arkoala/arkui-common/ohos-sdk-ets/HarmonyOS-NEXT-DB1/openharmony/ets/component/index-full.d.ts", "../../arkoala/arkui-common/ohos-sdk-ets/HarmonyOS-NEXT-DB1/openharmony/ets/component/koala-extensions.d.ts", "../../arkoala/arkui-common/ohos-sdk-ets/HarmonyOS-NEXT-DB1/openharmony/ets/api/@internal/full/global.d.ts", + "../../arkoala-arkts/arkui/src/index-test.d.ts", "../../incremental/tools/panda/arkts/std-lib/global.d.ts" ], // TODO: maybe delete this section "references": [ diff --git a/arkoala-arkts/ets-harness/.mocharc.json b/arkoala-arkts/ets-harness/.mocharc.json new file mode 100644 index 0000000000000000000000000000000000000000..1235551ee3645e39e42edb39985f942e013683ec --- /dev/null +++ b/arkoala-arkts/ets-harness/.mocharc.json @@ -0,0 +1,4 @@ +{ + "ui": "tdd", + "spec": "../build/index.js" +} \ No newline at end of file diff --git a/arkoala-arkts/ets-harness/arktsconfig.json b/arkoala-arkts/ets-harness/arktsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..e58b6495851a47b3322ffcc27e8a7e397069ae71 --- /dev/null +++ b/arkoala-arkts/ets-harness/arktsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "package": "@koalaui/ets-harness", + "outDir": "build/abc", + "rootDirs": ".", + "baseUrl": ".", + "paths": { + "#components": ["../arkui/build/unmemoized/src/generated/arkts"], + "#arkcompat/*": ["../../arkoala/arkui-common/build/unmemoized/src/arkts/*"], + "#arkstate": ["../../arkoala/arkui-common/build/unmemoized/src"], + "@koalaui/arkui-common": ["../../arkoala/arkui-common/build/unmemoized/src"], + "@koalaui/arkts-arkui": ["../arkui/build/unmemoized/src", "../arkui/build/unmemoized/src/peers"], + "@koalaui/interop": ["../../interop/src/arkts"], + "@koalaui/common": ["../../incremental/common/src"], + "@koalaui/compat": ["../../incremental/compat/src/arkts"], + "@koalaui/runtime": ["../../incremental/runtime/build/unmemoized/src"], + "@koalaui/harness": ["../../incremental/harness/src/arkts"] + } + }, + "exclude": [ + "build/unmemoized/src/loader.ts", + "build/unmemoized/src/test_entry.ts" + ], + "include": [ + "src/EtsHarnessApplication.ts", + "build/unmemoized/src/*.ts", + "build/unmemoized/build/generated/pages/*.ts" + ] +} diff --git a/arkoala-arkts/ets-harness/package.json b/arkoala-arkts/ets-harness/package.json new file mode 100644 index 0000000000000000000000000000000000000000..2138ab059a5127d60858da4a6a4dc37659642fbe --- /dev/null +++ b/arkoala-arkts/ets-harness/package.json @@ -0,0 +1,41 @@ +{ + "name": "@koalaui/ets-harness", + "version": "1.4.8+devel", + "description": "", + "main": "./build/lib/src/launcher.js", + "types": "./build/lib/src/launcher.d.ts", + "files": [ + "build/lib/**/*.js", + "build/lib/**/*.d.ts" + ], + "scripts": { + "clean": "rimraf build", + "compile:plugin": "cd ../../arkoala/ets-plugin && npm run compile", + "compile:ets": "npm run compile:plugin && cd src/ets && ets-tsc -p ./etsconfig.json", + "compile:unmemoize": "ets-tsc -p ./tsconfig-unmemoize.json", + "compile:loader": "WEBPACK_NO_MINIMIZE=true webpack --config webpack.config.node.js", + "compile:abc": "fast-arktsc --input-files ./arktsconfig.json --output-dir ./build --compiler ../../incremental/tools/panda/arkts/arktsc --link-name ets-harness && ninja ${NINJA_OPTIONS} -f build/build.ninja", + "compile:all": "npm run compile:ets && npm run compile:unmemoize && npm run compile:loader && npm run compile:abc", + "test:compile": "npm run compile:all && cp -r build/ets-harness.abc ../build/", + "run:pure": "ACE_LIBRARY_PATH=../build PANDA_HOME=../../incremental/tools/panda/node_modules/@panda/sdk bash ../../incremental/tools/panda/arkts/ark build/ets-harness.abc --ark-boot-files ../build/arkoala.abc:../../incremental/harness/build/harness.abc --ark-entry-point @koalaui.ets-harness.build.unmemoized.src.loader.ETSGLOBAL::main", + "run": "ACE_LIBRARY_PATH=../build PANDA_HOME=../../incremental/tools/panda/node_modules/@panda/sdk mocha ../build/index.js panda:EtsHarness", + "test:run": "npm run test:compile && npm run run" + }, + "keywords": [], + "dependencies": { + "@koalaui/common": "1.4.8+devel", + "@koalaui/compat": "1.4.8+devel", + "@koalaui/harness": "1.4.8+devel" + }, + "devDependencies": { + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.0", + "@typescript-eslint/eslint-plugin": "^5.20.0", + "@typescript-eslint/parser": "^5.20.0", + "chai": "^4.3.6", + "eslint": "^8.13.0", + "eslint-plugin-unused-imports": "^2.0.0", + "mocha": "^9.2.2", + "source-map-support": "^0.5.21" + } +} \ No newline at end of file diff --git a/arkoala-arkts/ets-harness/src/EtsHarnessApplication.ts b/arkoala-arkts/ets-harness/src/EtsHarnessApplication.ts new file mode 100644 index 0000000000000000000000000000000000000000..38bf4ba7c26a5b59ed2dc621fa254747539fb0e0 --- /dev/null +++ b/arkoala-arkts/ets-harness/src/EtsHarnessApplication.ts @@ -0,0 +1,176 @@ +import { ArkTestComponentPeer, checkArkoalaCallbacks, checkEvents, PeerNode, setCustomEventsChecker, UserView, UserViewBuilder } from "@koalaui/arkts-arkui" +import { int64 } from "@koalaui/common" +import { InteropNativeModule, KPointer, pointer, registerNativeModuleLibraryName } from "@koalaui/interop" +import { ComputableState, createAnimationTimer, GlobalStateManager, memoEntry, MutableState, StateContext, StateManager } from "@koalaui/runtime" +import { ArkUINativeModule } from "#components" +import { int32 } from "@koalaui/common" + +setCustomEventsChecker(checkArkoalaCallbacks) + +class PartialUpdateRecord { + public update: () => void + public context: Object + public callback: (before: boolean) => void + + constructor(update: () => void, context: Object, callback: (before: boolean) => void) { + this.callback = callback + this.context = context + this.update = update + } +} + +let partialUpdates = new Array() +let _currentPartialUpdateContext: Object | undefined = undefined + +/** + * Provide partial update lambda and context. + * + * @param update - function that performs state update + * @param context - context available to UI code when state update effect happens + */ +export function addPartialUpdate(update: () => void, context: T, callback: (before: boolean) => void): void { + partialUpdates.push(new PartialUpdateRecord(update, context as Object, callback)) +} + +/** + * Current partial update context or undefined. + * + * @returns current partial update context + */ +export function currentPartialUpdateContext(): T | undefined { + return _currentPartialUpdateContext as (T | undefined) +} + +let detachedRoots: Map> = new Map>() + +function createMemoRoot( + manager: StateManager, + /** @memo */ + builder: UserViewBuilder +): ComputableState { + const peer = PeerNode.generateRootPeer() + const node = manager.updatableNode(peer, (context: StateContext) => { + const frozen = manager.frozen + manager.frozen = true + memoEntry(context, 0, builder) + manager.frozen = frozen + }) + node.value + return node +} + + +export class EtsHarnessApplication { + private manager: StateManager | undefined = undefined + private root: ComputableState | undefined = undefined + private timer: MutableState | undefined = undefined + private userView: UserView | undefined = undefined + private appId: string = "EtsHarness" + + constructor(app: string, userView: UserView, useNativeLog: boolean) { + this.userView = userView + this.appId = app + } + + static createApplication(app: string, page: string, useNativeLog: boolean): EtsHarnessApplication { + registerNativeModuleLibraryName("InteropNativeModule", "ArkoalaNative_ark") + registerNativeModuleLibraryName("ArkUINativeModule", "ArkoalaNative_ark") + registerNativeModuleLibraryName("ArkUIGeneratedNativeModule", "ArkoalaNative_ark") + registerNativeModuleLibraryName("TestNativeModule", "ArkoalaNative_ark") + const userView = ArkUINativeModule._LoadUserView(app, page) + if (userView == undefined) throw new Error("Cannot load user view"); + return new EtsHarnessApplication(app, userView as UserView, useNativeLog) + } + + restartWith(page: string) { + if (this.manager === undefined) { + return + } + this.manager!.reset() + const userView = ArkUINativeModule._LoadUserView(this.appId, page) + if (userView == undefined) throw new Error("Cannot load user view"); + this.userView = userView as UserView + let + /** @memo */ + content = this.userView!.getBuilder() + this.root = createMemoRoot(this.manager!, content) + } + + start(): pointer { + this.manager = GlobalStateManager.instance + this.timer = createAnimationTimer(this.manager!) + let + /** @memo */ + content = this.userView!.getBuilder() + this.root = createMemoRoot(this.manager!, content) + return this.root!.value.peer.ptr + } + + enter(arg0: int32, arg1: int32): boolean { + this.timer!.value = Date.now() as int64 + checkEvents() + this.updateStates(this.manager!, this.root!) + return true + } + + emitEvent(type: int32, target: int32, arg0: int32, arg1: int32) { + const node = PeerNode.findPeerByNativeId(target) + switch (type) { + case 1: { + if (node != undefined) { + const peer = node as ArkTestComponentPeer + peer.onChangeCallback?.() + } + break + } + case 2: { + UserView.startNativeLog(1) + break; + } + case 3: { + UserView.stopNativeLog(1) + break; + } + default: { + InteropNativeModule._NativeLog(">>> Unsupported event type: " + type) + break; + } + } + } + + updateStates(manager: StateManager, root: ComputableState ) { + // Ensure all current state updates took effect. + manager.syncChanges() + manager.updateSnapshot() + root.value + for (const detachedRoot of detachedRoots.values()) + detachedRoot.value + if (partialUpdates.length > 0) { + // If there are pending partial updates - we apply them one by one and provide update context. + for (let update of partialUpdates) { + // Set the context available via currentPartialUpdateContext() to @memo code. + _currentPartialUpdateContext = update.context + // Update states. + update.update() + // Propagate changes. + manager.updateSnapshot() + // Notify subscriber. + update.callback(true) + // Compute new tree state + try { + root.value + for (const detachedRoot of detachedRoots.values()) + detachedRoot.value + } catch (error) { + InteropNativeModule._NativeLog('has error in partialUpdates') + } + // Notify subscriber. + update.callback(false) + // Clear context. + _currentPartialUpdateContext = undefined + } + // Clear partial updates list. + partialUpdates.splice(0, partialUpdates.length) + } + } +} diff --git a/arkoala-arkts/ets-harness/src/Page.ts b/arkoala-arkts/ets-harness/src/Page.ts new file mode 100644 index 0000000000000000000000000000000000000000..36611291b442d4612c2213fe027d1de419d06812 --- /dev/null +++ b/arkoala-arkts/ets-harness/src/Page.ts @@ -0,0 +1,28 @@ +import { Case1 } from "../build/generated/pages/case1" +import { Case2 } from "../build/generated/pages/case2" +import { UserView, UserViewBuilder } from "@koalaui/arkts-arkui" + +export class EtsHarness extends UserView { + private params: String + constructor(params: String) { + super() + this.params = params + } + getBuilder(): UserViewBuilder { + switch (this.params) { + case "Case1": { + /** @memo */ + const wrapper = () => { Case1() } + return wrapper + } + case "Case2": { + /** @memo */ + const wrapper = () => { Case2() } + return wrapper + } + default: { + throw new Error("No test case provided!") + } + } + } +} diff --git a/arkoala-arkts/ets-harness/src/ets/etsconfig.json b/arkoala-arkts/ets-harness/src/ets/etsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..bf870ef432d50f91245c44004b3f203fe17107e9 --- /dev/null +++ b/arkoala-arkts/ets-harness/src/ets/etsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../../arkoala-arkts/arkui/config/etsconfig-base.json", + "include": [ + "./**/*.ets" + ], + "compilerOptions": { + "types": [], + "baseUrl": ".", + "rootDirs": [ + "." + ], + "outDir": "../../build/ets-junk", + "plugins": [ + { + "transform": "@koalaui/ets-plugin/build/lib/src/ArkExpander.js", + "destination": "../../build/generated", + "arkui": "@koalaui/arkts-arkui" + } + ] + } +} diff --git a/arkoala-arkts/ets-harness/src/ets/pages/case1.ets b/arkoala-arkts/ets-harness/src/ets/pages/case1.ets new file mode 100644 index 0000000000000000000000000000000000000000..8594a52146b8ff833d3da9c834e46f83d0e43903 --- /dev/null +++ b/arkoala-arkts/ets-harness/src/ets/pages/case1.ets @@ -0,0 +1,12 @@ +//@Entry +@Component +struct Case1 { + @State x:number = 0 + build() { + TestComponent({ id: 42 }).onChange(() => { + this.x++ + console.log("Case1 - value:" + this.x) + }) + .log("Case1 - value:" + this.x) + } +} \ No newline at end of file diff --git a/arkoala-arkts/ets-harness/src/ets/pages/case2.ets b/arkoala-arkts/ets-harness/src/ets/pages/case2.ets new file mode 100644 index 0000000000000000000000000000000000000000..30529d8ff43399e65a4711f9e977a7b44a7bf47b --- /dev/null +++ b/arkoala-arkts/ets-harness/src/ets/pages/case2.ets @@ -0,0 +1,12 @@ +//@Entry +@Component +struct Case2 { + @State x:number = 0 + build() { + TestComponent({ id: 42 }).onChange(() => { + this.x-- + console.log("Case2 - value:" + this.x) + }) + .log("Case2 - value:" + this.x) + } +} \ No newline at end of file diff --git a/arkoala-arkts/ets-harness/src/loader.ts b/arkoala-arkts/ets-harness/src/loader.ts new file mode 100644 index 0000000000000000000000000000000000000000..586a48ad776d73d7c0253693835eb97f636d4636 --- /dev/null +++ b/arkoala-arkts/ets-harness/src/loader.ts @@ -0,0 +1,96 @@ +import { int8Array } from "@koalaui/compat" +import { entry } from "./test_entry" + +type int32 = number +type int64 = number +type float32 = number + +export type KStringPtr = string +export type KStringArrayPtr = Uint8Array +export type KInt32ArrayPtr = Int32Array +export type KFloat32ArrayPtr = Float32Array +export type KUint8ArrayPtr = Uint8Array +export type KInt = int32 +export type KUInt = int32 +export type KLong = int64 +export type KFloat = float32 +export type KBoolean = int32 +export type KPointer = bigint +export type pointer = KPointer +export type KNativePointer = KPointer + +export interface LoaderOps { + _LoadVirtualMachine(vmKind: int32, appClassPath: string, appLibPath: string): int32 + _StartApplication(appUrl: string, appParams: string): KPointer + _RunApplication(arg0: int32, arg1: int32): boolean +} + +export interface NativeControl extends LoaderOps { + _EmitEvent(type: int32, target: int32, arg0: int32, arg1: int32): void + _RestartWith(page: string): void +} + +let theModule: NativeControl | undefined = undefined + +declare const LOAD_NATIVE: object + +export function nativeModule(): NativeControl { + if (theModule) return theModule + theModule = new Object() as NativeControl + const modules = LOAD_NATIVE as any + if (!modules) + throw new Error("Cannot load native module") + for (const moduleName of Object.keys(modules)) { + Object.assign(theModule, modules[moduleName]) // TODO freeze? + } + return theModule +} + +export class AppControl { + getLog(): string { + return "" + } + emitTask(type: int32, target: int32, arg1: int32, arg2: int32): AppControl { + nativeModule()._EmitEvent(type, target, arg1, arg2) + return this + } + start(): AppControl { + nativeModule()._EmitEvent(2, -1, 0, 0) + return this + } + stop(): AppControl { + nativeModule()._EmitEvent(3, -1, 0, 0) + return this + } + nextFrame(): AppControl { + nativeModule()._RunApplication(0, 0) + return this + } + loadPage(page: string): AppControl { + nativeModule()._RestartWith(page) + return this + } +} + +let vm = "panda" +let app = "EtsHarness" +let params = "Case1" + +export function loadVM(variant: string, app: string, params: string, loopIterations?: number): void { + let classPath = __dirname + let nativePath = __dirname + + let vmKind = 2 // panda + const control = new AppControl() + + const result = nativeModule()._LoadVirtualMachine(vmKind, classPath, nativePath) + if (result == 0) { + nativeModule()._StartApplication(app, params); + control.nextFrame() + } else { + throw new Error(`Cannot start VM: ${result}`) + } + entry(control) +} + +loadVM(vm, app, params, (process && process.argv.length > 4) ? parseInt(process.argv[4]) : undefined) diff --git a/arkoala-arkts/ets-harness/src/test_entry.ts b/arkoala-arkts/ets-harness/src/test_entry.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec112fe5e519d4085f3f10c7974bb5992df68cb7 --- /dev/null +++ b/arkoala-arkts/ets-harness/src/test_entry.ts @@ -0,0 +1,33 @@ +import { Assert } from "@koalaui/harness" +import { AppControl } from "./loader" + +export function entry(control: AppControl) { + suite("Case1", () => { + test("StateChange:Increment", () => { + // onChange + control + .loadPage("Case1") + .start() + .emitTask(1, 42, 0, 0).nextFrame() + .emitTask(1, 42, 0, 0).nextFrame() + .emitTask(1, 42, 0, 0).nextFrame() + .stop() + let x = 3 + Assert.equal(x, 3, "StateChange test is failed!\n expected: " + 3 + "\ncurrent: " + x) + }) + }) + suite("Case2", () => { + test("StateChange:Decrement", () => { + // onChange + control + .loadPage("Case2") + .start() + .emitTask(1, 42, 0, 0).nextFrame() + .emitTask(1, 42, 0, 0).nextFrame() + .emitTask(1, 42, 0, 0).nextFrame() + .stop() + let x = -3 + Assert.equal(x, -3, "StateChange test is failed!\n expected: " + (-3) + "\ncurrent: " + x) + }) + }) +} diff --git a/arkoala-arkts/ets-harness/tsconfig-unmemoize.json b/arkoala-arkts/ets-harness/tsconfig-unmemoize.json new file mode 100644 index 0000000000000000000000000000000000000000..d0290c754158cf87710fc5466cddc9e1e8b8594d --- /dev/null +++ b/arkoala-arkts/ets-harness/tsconfig-unmemoize.json @@ -0,0 +1,56 @@ +{ + "extends": "@koalaui/build-common/tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "module": "CommonJS", + "plugins": [ + { + "transform": "@koalaui/compiler-plugin/build/lib/src/koala-transformer.js", + "trace": false, + "only_unmemoize": true, + "unmemoizeDir": "./build/unmemoized" + } + ], + "outDir": "./build/lib", + "baseUrl": ".", + "paths": { + "@koalaui/arkui-common": [ + "../../arkoala/arkui-common/src/arkts" + ], + "@koalaui/runtime": [ + "../../incremental/runtime/src" + ], + "@koalaui/harness": [ + "../../incremental/harness" + ], + "@koalaui/arkts-arkui": [ + "../arkui/src" + ], + "@koalaui/arkts-arkui/ohos.router": [ + "../arkui/src/ohos.router.ts" + ], + "#arkcompat": [ + "../../arkoala/arkui-common/src/arkts" + ], + "#components": [ + "../arkui/src/generated/ts" + ], + } + }, + "include": [ + "./build/generated", + "./src/Page.ts" + ], + "exclude": [ + ], + "references": [ + { "path": "../arkui" }, + { "path": "../../arkoala/arkui-common" }, + { "path": "../../arkoala/arkui-common/tsconfig-unmemoize.json" }, + { "path": "../arkui/tsconfig-unmemoize.json" }, + { "path": "../../incremental/runtime" }, + { "path": "../../incremental/compiler-plugin" }, + { "path": "../../incremental/common" }, + { "path": "../../incremental/harness" } + ] +} diff --git a/arkoala-arkts/ets-harness/tsconfig.json b/arkoala-arkts/ets-harness/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..98d0c0fbbae899ca78c912dfe4e0233765c605f5 --- /dev/null +++ b/arkoala-arkts/ets-harness/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2017", + "moduleResolution": "node", + "composite": true, + "incremental": true, + "declarationMap": true, + "sourceMap": true, + "declaration": true, + "noEmitOnError": true, + "strict": true, + "skipLibCheck": true, + "removeComments": false, + "outDir": "build", + }, + "include": [ + "src/loader.ts", + "src/test_entry.ts" + ] +} diff --git a/arkoala-arkts/ets-harness/webpack.config.node.js b/arkoala-arkts/ets-harness/webpack.config.node.js new file mode 100644 index 0000000000000000000000000000000000000000..47c01bb6203361b6cd3d0cc593394a1bfafe76ec --- /dev/null +++ b/arkoala-arkts/ets-harness/webpack.config.node.js @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022-2023 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. + */ + +const path = require("path") +const CopyPlugin = require("copy-webpack-plugin") +const DefinePlugin = require("webpack").DefinePlugin + +const minimize = !process.env.WEBPACK_NO_MINIMIZE + +/** @returns {import("webpack").WebpackOptionsNormalized} */ +const makeConfig = ({ os, arch, tsconfig }) => ({ + target: "node", + entry: `./src/loader.ts`, + output: { + filename: `index.js`, + path: path.resolve(__dirname, `../build`), + libraryTarget: "commonjs2", + }, + resolve: { + extensions: [".ts", ".node", "..."] + }, + + module: { + rules: [ + { + test: /\.tsx?$/, + loader: "ts-loader", + options: { + "projectReferences": true, + configFile: tsconfig, + compiler: "@koalaui/ets-tsc" + } + }, + ] + }, + + plugins: [ + new CopyPlugin({ + patterns: copyPluginPatterns(os, arch) + }), + new DefinePlugin({ + 'LOAD_NATIVE': `require("./ArkoalaLoader.node")` + }) + ], + + mode: minimize ? "production" : "development", + devtool: minimize ? false : "inline-source-map" +}) + +function getExt(os) { + switch (os) { + case "linux": return "so" + case "windows": return "dll" + case "macos": return "dylib" + default: return "so" + } +} + +function copyPluginPatterns(os, arch) { + const patterns = [] + patterns.push({ + from: path.resolve(`../../arkoala/framework/native/build-node-host-vmloader/ArkoalaLoader.node`), + to: "." + }) + patterns.push({ + from: path.resolve(`../../arkoala/framework/native/build-node-host-vmloader/libvmloader.${ getExt(os) }`), + to: "." + }) + patterns.push({ + from: path.resolve(`../../arkoala/framework/native/build-panda-host/libArkoalaNative_${os}_${arch}_ark.${ getExt(os) }`), + to: `./libArkoalaNative_ark.${ getExt(os) }` + }) + patterns.push({ + from: path.resolve(`../../arkoala/framework/native/build-panda-host/libace_compatible_mock.${ getExt(os) }`), + to: `./libace_compatible_mock.${ getExt(os) }` + }) + return patterns +} + +module.exports = env => { + const oses = { + 'win32': 'windows', + 'darwin': 'macos', + } + + const os = env.os || oses[process.platform] || process.platform + const arch = (env.arch || process.arch) + + const tsconfig = env.tsconfig || "" + + return makeConfig({os, arch, tsconfig}) +} diff --git a/arkoala-arkts/package.json b/arkoala-arkts/package.json index 2210b901374568511a41f72ba248fb90c9ebbef7..ee2066012bc889a8045bbd7ff77c1a9f3ae9ae4b 100644 --- a/arkoala-arkts/package.json +++ b/arkoala-arkts/package.json @@ -8,6 +8,7 @@ "./har", "../arkoala/arkui-common", "../arkoala/ets-plugin", + "./ets-harness", "../incremental/build-common", "../incremental/compiler-plugin", "../incremental/common", diff --git a/arkoala/framework/native/src/generated/bridge_custom.cc b/arkoala/framework/native/src/generated/bridge_custom.cc index ea077265d71b202f1aa55029b81685cb19940e0e..75972006fedf2a8e7f26ec881e46c34714a65f2b 100644 --- a/arkoala/framework/native/src/generated/bridge_custom.cc +++ b/arkoala/framework/native/src/generated/bridge_custom.cc @@ -465,6 +465,8 @@ KVMObjectHandle impl_LoadUserView(KVMContext vm, const KStringPtr& viewClass, co // TODO: hack, fix it! if (className == "ViewLoaderApp") { className = "Page.App"; + } else if (className == "EtsHarness") { + className = "@koalaui.ets-harness.build.unmemoized.src.Page.EtsHarness"; } else { className = "@koalaui.user.build.unmemoized.src.Page." + className; } diff --git a/arkoala/package.json b/arkoala/package.json index de8e2bbed04a7313cfc121cfa89d14eafb28a237..45b62727c2033069a70739b744564dc807b45f6c 100644 --- a/arkoala/package.json +++ b/arkoala/package.json @@ -18,6 +18,7 @@ "../incremental/common", "../incremental/runtime", "../incremental/compiler-plugin", + "../incremental/harness", "../interop", "../arkoala-arkts/arkui" ], diff --git a/interop/src/arkts/InteropNativeModule.sts b/interop/src/arkts/InteropNativeModule.sts index 9b74e2e839b506c42da1ccc6a87858de9affd7eb..87d73e4c8b5266b3afa9c1137f263b84a6231cbd 100644 --- a/interop/src/arkts/InteropNativeModule.sts +++ b/interop/src/arkts/InteropNativeModule.sts @@ -38,5 +38,6 @@ export class InteropNativeModule { native static _StartApplication(appUrl: string, appParams: string): KPointer native static _EmitEvent(eventType: int32, target: int32, arg0: int32, arg1: int32): void native static _CallForeignVM(context:KPointer, callback: int32, data: KUint8ArrayPtr, dataLength: int32): int32 + native static _RestartWith(page: string): void } diff --git a/interop/src/cpp/common-interop.cc b/interop/src/cpp/common-interop.cc index ff493c06e856deb8297be41c5011d2c88657ff0b..c522cc84ab0bd84559822dbc4005e071c0c42f42 100644 --- a/interop/src/cpp/common-interop.cc +++ b/interop/src/cpp/common-interop.cc @@ -260,6 +260,7 @@ typedef KInt (*LoadVirtualMachine_t)(KInt vmKind, const char* classPath, const c typedef KNativePointer (*StartApplication_t)(const char* appUrl, const char* appParams); typedef KBoolean (*RunApplication_t)(const KInt arg0, const KInt arg1); typedef void (*EmitEvent_t)(const KInt type, const KInt target, const KInt arg0, const KInt arg1); +typedef void (*RestartWith_t)(const char* page); void* getImpl(const char* path, const char* name) { static void* lib = nullptr; @@ -316,6 +317,13 @@ void impl_EmitEvent(const KInt type, const KInt target, const KInt arg0, const K } KOALA_INTEROP_V4(EmitEvent, KInt, KInt, KInt, KInt) +void impl_RestartWith(const KStringPtr& page) { + static RestartWith_t impl = nullptr; + if (!impl) impl = reinterpret_cast(getImpl(nullptr, "RestartWith")); + impl(page.c_str()); +} +KOALA_INTEROP_V1(RestartWith, KStringPtr) + static Callback_Caller_t g_callbackCaller = nullptr; void setCallbackCaller(Callback_Caller_t callbackCaller) { g_callbackCaller = callbackCaller; diff --git a/interop/src/cpp/vmloader.cc b/interop/src/cpp/vmloader.cc index dcd9bd6e2ec69ecbd3e881d167396ce183cc233d..d409a87809cda49b24ca74dfb50627c35307f5f4 100644 --- a/interop/src/cpp/vmloader.cc +++ b/interop/src/cpp/vmloader.cc @@ -140,6 +140,7 @@ struct VMEntry { void* app; void* enter; void* emitEvent; + void* restartWith; ForeignVMContext foreignVMContext; }; @@ -295,6 +296,8 @@ struct AppInfo { const char* enterMethodSig; const char* emitEventMethodName; const char* emitEventMethodSig; + const char* restartWithMethodName; + const char* restartWithMethodSig; }; #ifdef KOALA_JNI @@ -323,15 +326,29 @@ const AppInfo pandaAppInfo = { "emitEvent", "IIII:V", }; +const AppInfo harnessAppInfo = { + "@koalaui/ets-harness/src/EtsHarnessApplication/EtsHarnessApplication", + "createApplication", + "Lstd/core/String;Lstd/core/String;Z:L@koalaui/ets-harness/src/EtsHarnessApplication/EtsHarnessApplication;", + "start", + ":J", + "enter", + "II:Z", + "emitEvent", + "IIII:V", + "restartWith", + "Lstd/core/String;:V" +}; #endif extern "C" DLL_EXPORT KNativePointer StartApplication(const char* appUrl, const char* appParams) { + const auto isTestEnv = std::string(appUrl) == "EtsHarness"; const AppInfo* appInfo = #ifdef KOALA_JNI (g_vmEntry.vmKind == JAVA_VM_KIND) ? &javaAppInfo : #endif #ifdef KOALA_ETS_NAPI - (g_vmEntry.vmKind == PANDA_VM_KIND) ? &pandaAppInfo : + (g_vmEntry.vmKind == PANDA_VM_KIND) ? isTestEnv ? &harnessAppInfo : &pandaAppInfo : #endif nullptr; @@ -431,6 +448,17 @@ extern "C" DLL_EXPORT KNativePointer StartApplication(const char* appUrl, const } return nullptr; } + if (isTestEnv) { + g_vmEntry.restartWith = (void*)(etsEnv->Getp_method(appClass, appInfo->restartWithMethodName, appInfo->restartWithMethodSig)); + if (!g_vmEntry.restartWith) { + LOGE("Cannot find enter restartWith %" LOG_PUBLIC "s", appInfo->restartWithMethodSig); + if (etsEnv->ErrorCheck()) { + etsEnv->ErrorDescribe(); + etsEnv->ErrorClear(); + } + return nullptr; + } + } // TODO: pass app entry point! return reinterpret_cast(etsEnv->CallLongMethod((ets_object)(app), start)); } @@ -527,6 +555,46 @@ extern "C" DLL_EXPORT void EmitEvent(const KInt type, const KInt target, const K #endif } +extern "C" DLL_EXPORT void RestartWith(const char* page) { +#ifdef KOALA_JNI + if (g_vmEntry.vmKind == JAVA_VM_KIND) { + JNIEnv* jEnv = (JNIEnv*)(g_vmEntry.env); + if (!g_vmEntry.restartWith) { + LOGE("Cannot find restartWith method"); + return; + } + jEnv->CallVoidMethod( + (jobject)(g_vmEntry.app), + (jmethodID)(g_vmEntry.restartWith), + jEnv->NewStringUTF(page) + ); + if (jEnv->ExceptionCheck()) { + jEnv->ExceptionDescribe(); + jEnv->ExceptionClear(); + } + } +#endif +#ifdef KOALA_ETS_NAPI + if (g_vmEntry.vmKind == PANDA_VM_KIND) { + EtsEnv* etsEnv = (EtsEnv*)(g_vmEntry.env); + if (!g_vmEntry.restartWith) { + LOGE("Cannot find restartWith method"); + return; + } + etsEnv->CallVoidMethod( + (ets_object)(g_vmEntry.app), + (ets_method)(g_vmEntry.restartWith), + etsEnv->NewStringUTF(page) + ); + if (etsEnv->ErrorCheck()) { + LOGE("Calling restartWith() method gave an error"); + etsEnv->ErrorDescribe(); + etsEnv->ErrorClear(); + } + } + #endif +} + void traverseDir(std::string root, std::vector& paths, int depth) { if (depth >= 50) { return;