| @@ -0,0 +1,18 @@ | |||
| 1 | +import React from 'react'; | ||
| 2 | +import {SafeAreaView, StatusBar, StyleSheet} from 'react-native'; | ||
| 3 | + | ||
| 4 | +import {HomePage} from './src/pages/HomePage'; | ||
| 5 | + | ||
| 6 | +export default function App(): React.JSX.Element { | ||
| 7 | + return ( | ||
| 8 | + <SafeAreaView style={styles.root}> | ||
| 9 | + <StatusBar barStyle="dark-content" backgroundColor="#F4F6FA" /> | ||
| 10 | + <HomePage /> | ||
| 11 | + </SafeAreaView> | ||
| 12 | + ); | ||
| 13 | +} | ||
| 14 | + | ||
| 15 | +const styles = StyleSheet.create({ | ||
| 16 | + root: {flex: 1, backgroundColor: '#F4F6FA'}, | ||
| 17 | +}); | ||
| 18 | + | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | +# 模板来源 | ||
| 2 | + | ||
| 3 | +HarmonyOS 宿主与 CMake 接入沿用本机已有的 RNOH 0.72 系列示例骨架。框架由 npm 依赖取得,依赖中的原始许可证继续适用。首页、公共用例卡片和 JSON 示例是此展示 Demo 的代码。 | ||
| 4 | + | ||
| 5 | +模板没有包含被测库实现或个人签名凭据。共享源码请按 README 排除缓存和构建输出。 | ||
| 6 | + | ||
| @@ -0,0 +1,87 @@ | |||
| 1 | +# @react-native-ohos/datetimepicker 用例展示 Demo(br_rnoh0.72) | ||
| 2 | + | ||
| 3 | +基于「用例展示 Demo」模板搭建,用于演示 `@react-native-ohos/datetimepicker` 7.6.3 | ||
| 4 | +(本仓库 `harmony/datetimepicker` + 根 `src/`)在 `br_rnoh0.72` 分支上的鸿蒙适配能力。 | ||
| 5 | +本分支是该库首个 OpenHarmony 适配版本(7.6.2 社区基线),因此用例覆盖适配后的核心交互。 | ||
| 6 | + | ||
| 7 | +所有用例卡片直接平铺在首页,每张卡片展示说明、被测控件、执行按钮、实际结果和执行结论。 | ||
| 8 | +卡片样式、`CaseCard`、`HomePage` 与模板保持一致,未改动公共组件。 | ||
| 9 | + | ||
| 10 | +## 用例清单 | ||
| 11 | + | ||
| 12 | +| 编号 | 覆盖能力 | 用例文件 | | ||
| 13 | +|---|---|---| | ||
| 14 | +| `DTP-072-01` | date + spinner 滚动产生 `set` 事件与 `timestamp` | `DateSpinnerChangeCase.tsx` | | ||
| 15 | +| `DTP-072-02` | time 模式 `is24Hour` 两种制式渲染 | `TimeIs24HourCase.tsx` | | ||
| 16 | +| `DTP-072-03` | date + compact 展开日历并选择日期 | `DateCompactCase.tsx` | | ||
| 17 | +| `DTP-072-04` | date + spinner 运行时更新 `value` | `DynamicValueCase.tsx` | | ||
| 18 | + | ||
| 19 | +用例需要先在被测控件上完成一次真实操作(滚动/选择/切换),再点「执行用例」读取回调和状态。 | ||
| 20 | +公共组件不比较业务结果,结论由用例基于真实观察给出。 | ||
| 21 | + | ||
| 22 | +## 接入被测库 | ||
| 23 | + | ||
| 24 | +Demo 与库源码同仓库: | ||
| 25 | + | ||
| 26 | +- JS:`src/datetimepicker.ts` 直接 `require('../../src/index.js')` 指向当前分支的库入口, | ||
| 27 | + Metro 通过 `watchFolders` 监视仓库根目录。未在 Demo 内安装被测库 npm 包; | ||
| 28 | + `package.json` 里的 `@react-native-community/datetimepicker` 是库 `src/` 引用的上游社区包。 | ||
| 29 | +- ArkTS:`harmony/entry/src/main/ets/pages/Index.ets` 按包名 import `RNDateTimePicker` / | ||
| 30 | + `DATETIME_PICKER_VIEW_TYPE` 并注册自定义组件。 | ||
| 31 | +- Native:`harmony/entry/src/main/cpp/CMakeLists.txt` 手动 `add_subdirectory` 库的 cpp, | ||
| 32 | + `PackageProvider.cpp` 注册 `DateTimePickerPackage`。 | ||
| 33 | +- 包依赖:`harmony/entry/oh-package.json5` 依赖本分支已提交的 | ||
| 34 | + `harmony/datetimepicker.har`;`harmony/build-profile.json5` 同时把 | ||
| 35 | + `../../harmony/datetimepicker` 声明为 module,供 hvigor 编译库自身 ArkTS。 | ||
| 36 | + | ||
| 37 | +## 卡片与结果 | ||
| 38 | + | ||
| 39 | +`src/components/CaseCard.tsx` 负责统一展示与执行。`src/models/testCase.ts` 定义字段: | ||
| 40 | +编号、标题、测试目的、步骤、预期结果、真实执行函数,以及可选的测试接口、异常表现、 | ||
| 41 | +超时与进程终止风险提示。 | ||
| 42 | + | ||
| 43 | +执行前实际结果和结论为空。用例通过 `context.write('真实观察')` 追加结果, | ||
| 44 | +用 `context.decide('通过')` / `'未通过'` / `'需要人工判断'` 设置结论。 | ||
| 45 | +公共组件不比较业务结果,也不填写固定成功文本。 | ||
| 46 | + | ||
| 47 | +## 环境与构建 | ||
| 48 | + | ||
| 49 | +使用 Node.js、DevEco Studio、ohpm、hvigorw,以及 PowerShell 7。 | ||
| 50 | +框架组合为 React 18.2.0、React Native 0.72.5、React Native Harmony 0.72.139。 | ||
| 51 | +HarmonyOS 宿主在 `harmony/`,目标 ARM64,runtimeOS 为 HarmonyOS,compatibleSdkVersion 5.0.0(12)。 | ||
| 52 | +应用包名为 `com.example.datetimepicker.casedemo072`,与其他分支的 Demo 区分,可在同一设备并行安装对比。 | ||
| 53 | + | ||
| 54 | +```powershell | ||
| 55 | +pwsh -File scripts/build.ps1 | ||
| 56 | +pwsh -File scripts/build.ps1 -SigningConfig "本机 signing.json 的绝对路径" | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +从零拉取后跑通(顺序不能省): | ||
| 60 | + | ||
| 61 | +```cmd | ||
| 62 | +cd case_demo | ||
| 63 | +npm install --legacy-peer-deps | ||
| 64 | + | ||
| 65 | +cd harmony | ||
| 66 | +ohpm install | ||
| 67 | +cd .. | ||
| 68 | + | ||
| 69 | +npx react-native codegen-harmony --rnoh-module-path .\harmony\entry\oh_modules\@rnoh\react-native-openharmony | ||
| 70 | +npm run bundle:harmony | ||
| 71 | + | ||
| 72 | +cd harmony | ||
| 73 | +hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon | ||
| 74 | +``` | ||
| 75 | + | ||
| 76 | +`ohpm install` 必须先于 codegen:`codegen-harmony` 需要读取解包后的 | ||
| 77 | +`harmony/entry/oh_modules/@rnoh/react-native-openharmony`,生成 | ||
| 78 | +`entry/src/main/cpp/generated/RNOHGeneratedPackage.h`。缺少该文件会在 `BuildNativeWithNinja` | ||
| 79 | +阶段报 `'generated/RNOHGeneratedPackage.h' file not found`。 | ||
| 80 | +`npm install` 受网络限制时可先配置镜像,例如 `npm config set registry https://registry.npmmirror.com`。 | ||
| 81 | + | ||
| 82 | +`bundle:harmony` 会把 JS bundle 写到 `harmony/entry/src/main/resources/rawfile/bundle.harmony.js`, | ||
| 83 | +该文件与 `node_modules`、`oh_modules`、`entry/.cxx`、`entry/build` 均不入库。 | ||
| 84 | + | ||
| 85 | +换分支或换依赖后必须删除 `entry/.cxx`、`entry/build` 再构建,否则 CMake 会继续使用上一轮 | ||
| 86 | +解包的 RNOH har。签名配置只在构建期间应用,结束后恢复原始工程配置;无签名配置时产出 | ||
| 87 | +unsigned HAP。共享模板不包含证书、口令或个人签名材料。 | ||
| @@ -0,0 +1,4 @@ | |||
| 1 | +{ | ||
| 2 | + "name": "CaseDemoDateTimePicker", | ||
| 3 | + "displayName": "DateTimePicker 用例展示 Demo" | ||
| 4 | +} | ||
| @@ -0,0 +1,4 @@ | |||
| 1 | +module.exports = { | ||
| 2 | + presets: ['module:metro-react-native-babel-preset'], | ||
| 3 | +}; | ||
| 4 | + | ||
| @@ -0,0 +1,10 @@ | |||
| 1 | +{ | ||
| 2 | + "app": { | ||
| 3 | + "bundleName": "com.example.datetimepicker.casedemo072", | ||
| 4 | + "vendor": "example", | ||
| 5 | + "versionCode": 1000000, | ||
| 6 | + "versionName": "1.0.0", | ||
| 7 | + "icon": "$media:app_icon", | ||
| 8 | + "label": "$string:app_name" | ||
| 9 | + } | ||
| 10 | +} | ||
| @@ -0,0 +1,32 @@ | |||
| 1 | +{ | ||
| 2 | + "files": ["**/*.ts", "**/*.ets"], | ||
| 3 | + "ignore": [ | ||
| 4 | + "**/ohosTest/**/*", | ||
| 5 | + "**/node_modules/**/*", | ||
| 6 | + "**/hvigorfile.ts", | ||
| 7 | + "**/node_modules/**/*", | ||
| 8 | + "**/oh_modules/**/*", | ||
| 9 | + "**/build/**/*", | ||
| 10 | + "**/.preview/**/*" | ||
| 11 | + ], | ||
| 12 | + "plugins": ["@typescript-eslint"], | ||
| 13 | + "ruleSet": [], | ||
| 14 | + "rules": { | ||
| 15 | + "@typescript-eslint/await-thenable": "warn", | ||
| 16 | + "@typescript-eslint/consistent-type-imports": "warn", | ||
| 17 | + "@typescript-eslint/explicit-function-return-type": "warn", | ||
| 18 | + "@typescript-eslint/explicit-module-boundary-types": "warn", | ||
| 19 | + "@typescript-eslint/no-dynamic-delete": "warn", | ||
| 20 | + "@typescript-eslint/no-explicit-any": "warn", | ||
| 21 | + "@typescript-eslint/no-for-in-array": "warn", | ||
| 22 | + "@typescript-eslint/no-this-alias": "warn", | ||
| 23 | + "@typescript-eslint/no-unnecessary-type-constraint": "warn", | ||
| 24 | + "@typescript-eslint/no-unsafe-argument": "warn", | ||
| 25 | + "@typescript-eslint/no-unsafe-assignment": "warn", | ||
| 26 | + "@typescript-eslint/no-unsafe-call": "warn", | ||
| 27 | + "@typescript-eslint/no-unsafe-member-access": "warn", | ||
| 28 | + "@typescript-eslint/no-unsafe-return": "warn", | ||
| 29 | + "@typescript-eslint/prefer-literal-enum-member": "warn" | ||
| 30 | + }, | ||
| 31 | + "overrides": [] | ||
| 32 | +} | ||
| @@ -0,0 +1,7 @@ | |||
| 1 | +/node_modules | ||
| 2 | +/.preview | ||
| 3 | +/build | ||
| 4 | +/.cxx | ||
| 5 | +package-lock.json | ||
| 6 | +/oh_modules | ||
| 7 | +/assets | ||
| @@ -0,0 +1,19 @@ | |||
| 1 | +{ | ||
| 2 | + "apiType": "stageMode", | ||
| 3 | + "buildOption": { | ||
| 4 | + "externalNativeOptions": { | ||
| 5 | + "path": "./src/main/cpp/CMakeLists.txt", | ||
| 6 | + "arguments": "", | ||
| 7 | + "cppFlags": "-s", | ||
| 8 | + "abiFilters": [ | ||
| 9 | + "arm64-v8a" | ||
| 10 | + ] | ||
| 11 | + } | ||
| 12 | + }, | ||
| 13 | + "targets": [ | ||
| 14 | + { | ||
| 15 | + "name": "default", | ||
| 16 | + "runtimeOS": "HarmonyOS" | ||
| 17 | + } | ||
| 18 | + ] | ||
| 19 | +} | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved | ||
| 3 | + * Use of this source code is governed by a MIT license that can be | ||
| 4 | + * found in the LICENSE file. | ||
| 5 | + */ | ||
| 6 | + | ||
| 7 | +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. | ||
| 8 | +export { hapTasks } from '@ohos/hvigor-ohos-plugin'; | ||
| @@ -0,0 +1,11 @@ | |||
| 1 | +{ | ||
| 2 | + "license": "ISC", | ||
| 3 | + "devDependencies": {}, | ||
| 4 | + "name": "entry", | ||
| 5 | + "description": "datetimepicker 用例展示 Demo 宿主", | ||
| 6 | + "version": "1.0.0", | ||
| 7 | + "dependencies": { | ||
| 8 | + "@rnoh/react-native-openharmony": "file:../../node_modules/@react-native-oh/react-native-harmony/react_native_openharmony.har", | ||
| 9 | + "@react-native-ohos/datetimepicker": "file:../../../harmony/datetimepicker.har" | ||
| 10 | + } | ||
| 11 | +} | ||
| @@ -0,0 +1,2 @@ | |||
| 1 | +jsbundle.h | ||
| 2 | +generated/ | ||
| @@ -0,0 +1,43 @@ | |||
| 1 | +project(rnapp) | ||
| 2 | +cmake_minimum_required(VERSION 3.4.1) | ||
| 3 | +set(CMAKE_SKIP_BUILD_RPATH TRUE) | ||
| 4 | +set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}") | ||
| 5 | +set(NODE_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../node_modules") | ||
| 6 | +set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules") | ||
| 7 | +set(RNOH_CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules/@rnoh/react-native-openharmony/src/main/cpp") | ||
| 8 | +set(LOG_VERBOSITY_LEVEL 1) | ||
| 9 | +set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments") | ||
| 10 | +set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie") | ||
| 11 | +set(OH_MODULES "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules") | ||
| 12 | +set(OH_Harmony "${CMAKE_CURRENT_SOURCE_DIR}/../../../..") | ||
| 13 | + | ||
| 14 | +set(WITH_HITRACE_SYSTRACE 1) # for other CMakeLists.txt files to use | ||
| 15 | +add_compile_definitions(WITH_HITRACE_SYSTRACE) | ||
| 16 | + | ||
| 17 | +# (VM) Define a variable and assign it to the current module's cpp directory | ||
| 18 | +set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) | ||
| 19 | + | ||
| 20 | +# Add the Header File directory, including cpp, cpp/include, and tell cmake to find the Header Files introduced by the code here | ||
| 21 | +include_directories(${NATIVERENDER_ROOT_PATH} | ||
| 22 | + ${NATIVERENDER_ROOT_PATH}/include) | ||
| 23 | + | ||
| 24 | +add_subdirectory("${RNOH_CPP_DIR}" ./rn) | ||
| 25 | + | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +# RNOH_BEGIN: manual_package_linking_1 | ||
| 29 | +add_subdirectory("${OH_MODULE_DIR}/@react-native-ohos/datetimepicker/src/main/cpp" ./datetimepicker) | ||
| 30 | +# RNOH_END: manual_package_linking_1 | ||
| 31 | + | ||
| 32 | +file(GLOB GENERATED_CPP_FILES "${CMAKE_CURRENT_SOURCE_DIR}/generated/*.cpp") # this line is needed by codegen v1 | ||
| 33 | + | ||
| 34 | +add_library(rnoh_app SHARED | ||
| 35 | + ${GENERATED_CPP_FILES} | ||
| 36 | + "./PackageProvider.cpp" | ||
| 37 | + "${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp" | ||
| 38 | +) | ||
| 39 | +target_link_libraries(rnoh_app PUBLIC rnoh) | ||
| 40 | + | ||
| 41 | +# RNOH_BEGIN: manual_package_linking_2 | ||
| 42 | +target_link_libraries(rnoh_app PUBLIC rnoh_datetime_picker) | ||
| 43 | +# RNOH_END: manual_package_linking_2 | ||
| @@ -0,0 +1,17 @@ | |||
| 1 | +// Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved | ||
| 2 | +// Use of this source code is governed by a MIT license that can be | ||
| 3 | +// found in the LICENSE file. | ||
| 4 | + | ||
| 5 | +#include "RNOH/PackageProvider.h" | ||
| 6 | +#include "generated/RNOHGeneratedPackage.h" | ||
| 7 | +#include "DateTimePickerPackage.h" | ||
| 8 | + | ||
| 9 | +using namespace rnoh; | ||
| 10 | + | ||
| 11 | +std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) | ||
| 12 | +{ | ||
| 13 | + return { | ||
| 14 | + std::make_shared<RNOHGeneratedPackage>(ctx), // generated by codegen v1 | ||
| 15 | + std::make_shared<DateTimePickerPackage>(ctx), | ||
| 16 | + }; | ||
| 17 | +} | ||
| @@ -0,0 +1,13 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved | ||
| 3 | + * Use of this source code is governed by a MIT license that can be | ||
| 4 | + * found in the LICENSE file. | ||
| 5 | + */ | ||
| 6 | + | ||
| 7 | + | ||
| 8 | +import type {RNPackageContext, RNPackage} from '@rnoh/react-native-openharmony/ts'; | ||
| 9 | + | ||
| 10 | +export function createRNPackages(ctx: RNPackageContext): RNPackage[] { | ||
| 11 | + return [ | ||
| 12 | + ]; | ||
| 13 | +} | ||
| @@ -0,0 +1,18 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved | ||
| 3 | + * Use of this source code is governed by a MIT license that can be | ||
| 4 | + * found in the LICENSE file. | ||
| 5 | + */ | ||
| 6 | + | ||
| 7 | +import {RNAbility} from '@rnoh/react-native-openharmony'; | ||
| 8 | +import { Want } from '@kit.AbilityKit'; | ||
| 9 | +export default class EntryAbility extends RNAbility { | ||
| 10 | + | ||
| 11 | + onCreate(want: Want) { | ||
| 12 | + super.onCreate(want) | ||
| 13 | + } | ||
| 14 | + | ||
| 15 | + getPagePath() { | ||
| 16 | + return 'pages/Index'; | ||
| 17 | + } | ||
| 18 | +} | ||
| @@ -0,0 +1,103 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved | ||
| 3 | + * Use of this source code is governed by a MIT license that can be | ||
| 4 | + * found in the LICENSE file. | ||
| 5 | + */ | ||
| 6 | + | ||
| 7 | +import { | ||
| 8 | + AnyJSBundleProvider, | ||
| 9 | + ComponentBuilderContext, | ||
| 10 | + FileJSBundleProvider, | ||
| 11 | + MetroJSBundleProvider, | ||
| 12 | + ResourceJSBundleProvider, | ||
| 13 | + RNApp, | ||
| 14 | + RNOHErrorDialog, | ||
| 15 | + RNOHLogger, | ||
| 16 | + TraceJSBundleProviderDecorator, | ||
| 17 | + RNOHCoreContext | ||
| 18 | +} from '@rnoh/react-native-openharmony'; | ||
| 19 | +import { createRNPackages } from '../RNPackagesFactory'; | ||
| 20 | +import { RNDateTimePicker, DATETIME_PICKER_VIEW_TYPE } from '@react-native-ohos/datetimepicker'; | ||
| 21 | + | ||
| 22 | +const arkTsComponentNames: Array<string> = [ | ||
| 23 | + DATETIME_PICKER_VIEW_TYPE | ||
| 24 | +]; | ||
| 25 | + | ||
| 26 | +@Builder | ||
| 27 | +export function buildCustomRNComponent(ctx: ComponentBuilderContext) { | ||
| 28 | + if (ctx.componentName === DATETIME_PICKER_VIEW_TYPE) { | ||
| 29 | + RNDateTimePicker({ | ||
| 30 | + ctx: ctx.rnComponentContext, | ||
| 31 | + tag: ctx.tag, | ||
| 32 | + }) | ||
| 33 | + } | ||
| 34 | + // There seems to be a problem with the placement of ArkTS components in mixed mode. Nested Stack temporarily avoided. | ||
| 35 | + Stack() { | ||
| 36 | + } | ||
| 37 | + .position({ x: 0, y: 0 }) | ||
| 38 | +} | ||
| 39 | + | ||
| 40 | +const wrappedCustomRNComponentBuilder = wrapBuilder(buildCustomRNComponent) | ||
| 41 | + | ||
| 42 | +@Entry | ||
| 43 | +@Component | ||
| 44 | +struct Index { | ||
| 45 | + @StorageLink('RNOHCoreContext') private rnohCoreContext: RNOHCoreContext | undefined = undefined | ||
| 46 | + @State shouldShow: boolean = false | ||
| 47 | + private logger!: RNOHLogger | ||
| 48 | + | ||
| 49 | + aboutToAppear() { | ||
| 50 | + this.logger = this.rnohCoreContext!.logger.clone("Index") | ||
| 51 | + const stopTracing = this.logger.clone("aboutToAppear").startTracing() | ||
| 52 | + | ||
| 53 | + this.shouldShow = true | ||
| 54 | + stopTracing() | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + onBackPress(): boolean | undefined { | ||
| 58 | + // NOTE: this is required since `Ability`'s `onBackPressed` function always | ||
| 59 | + // terminates or puts the app in the background, but we want Ark to ignore it completely | ||
| 60 | + // when handled by the application layer | ||
| 61 | + this.rnohCoreContext!.dispatchBackPress() | ||
| 62 | + return true | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + build() { | ||
| 66 | + Column() { | ||
| 67 | + if (this.rnohCoreContext && this.shouldShow) { | ||
| 68 | + if (this.rnohCoreContext?.isDebugModeEnabled) { | ||
| 69 | + RNOHErrorDialog({ ctx: this.rnohCoreContext }) | ||
| 70 | + } | ||
| 71 | + RNApp({ | ||
| 72 | + rnInstanceConfig: { | ||
| 73 | + createRNPackages, | ||
| 74 | + enableNDKTextMeasuring: true, | ||
| 75 | + enableBackgroundExecutor: false, | ||
| 76 | + enableCAPIArchitecture: true, | ||
| 77 | + arkTsComponentNames: arkTsComponentNames, | ||
| 78 | + }, | ||
| 79 | + initialProps: { "foo": "bar" } as Record<string, string>, | ||
| 80 | + appKey: "CaseDemoDateTimePicker", | ||
| 81 | + wrappedCustomRNComponentBuilder: wrappedCustomRNComponentBuilder, | ||
| 82 | + onSetUp: (rnInstance) => { | ||
| 83 | + rnInstance.enableFeatureFlag("ENABLE_RN_INSTANCE_CLEAN_UP") | ||
| 84 | + }, | ||
| 85 | + jsBundleProvider: new TraceJSBundleProviderDecorator( | ||
| 86 | + new AnyJSBundleProvider([ | ||
| 87 | + new MetroJSBundleProvider(), | ||
| 88 | + // NOTE: to load the bundle from file, place it in | ||
| 89 | + // `/data/app/el2/100/base/com.rnoh.tester/files/bundle.harmony.js` | ||
| 90 | + // on your device. The path mismatch is due to app sandboxing on HarmonyOS | ||
【AI-Review】【建议】【编码规范】【注释/可读性】
● 问题: ● 影响: 若使用者按注释把 bundle 推到 ● 建议: 将注释中的示例路径更新为本 Demo 沙箱对应的路径,或直接删除该 NOTE,避免与实际 ![]() ![]() | |||
| 91 | + new FileJSBundleProvider('/data/storage/el2/base/files/bundle.harmony.js'), | ||
| 92 | + new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'hermes_bundle.hbc'), | ||
| 93 | + new ResourceJSBundleProvider(this.rnohCoreContext.uiAbilityContext.resourceManager, 'bundle.harmony.js') | ||
| 94 | + ]), | ||
| 95 | + this.rnohCoreContext.logger), | ||
| 96 | + }) | ||
| 97 | + } | ||
| 98 | + Text("DateTimePicker 用例展示 Demo") | ||
| 99 | + } | ||
| 100 | + .height('100%') | ||
| 101 | + .width('100%') | ||
| 102 | + } | ||
| 103 | +} | ||
| @@ -0,0 +1,41 @@ | |||
| 1 | +{ | ||
| 2 | + "module": { | ||
| 3 | + "name": "entry", | ||
| 4 | + "type": "entry", | ||
| 5 | + "description": "$string:module_desc", | ||
| 6 | + "mainElement": "EntryAbility", | ||
| 7 | + "deviceTypes": [ | ||
| 8 | + "default" | ||
| 9 | + ], | ||
| 10 | + "deliveryWithInstall": true, | ||
| 11 | + "installationFree": false, | ||
| 12 | + "pages": "$profile:main_pages", | ||
| 13 | + "requestPermissions": [ | ||
| 14 | + { | ||
| 15 | + "name": "ohos.permission.INTERNET" | ||
| 16 | + } | ||
| 17 | + ], | ||
| 18 | + "abilities": [ | ||
| 19 | + { | ||
| 20 | + "name": "EntryAbility", | ||
| 21 | + "srcEntry": "./ets/entryability/EntryAbility.ets", | ||
| 22 | + "description": "$string:EntryAbility_desc", | ||
| 23 | + "icon": "$media:icon", | ||
| 24 | + "label": "$string:EntryAbility_label", | ||
| 25 | + "startWindowIcon": "$media:icon", | ||
| 26 | + "startWindowBackground": "$color:start_window_background", | ||
| 27 | + "visible": true, | ||
| 28 | + "skills": [ | ||
| 29 | + { | ||
| 30 | + "entities": [ | ||
| 31 | + "entity.system.home" | ||
| 32 | + ], | ||
| 33 | + "actions": [ | ||
| 34 | + "action.system.home" | ||
| 35 | + ] | ||
| 36 | + } | ||
| 37 | + ] | ||
| 38 | + } | ||
| 39 | + ] | ||
| 40 | + } | ||
| 41 | +} | ||
| @@ -0,0 +1 @@ | |||
| 1 | +rawfile/assets | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | +{ | ||
| 2 | + "color": [ | ||
| 3 | + { | ||
| 4 | + "name": "start_window_background", | ||
| 5 | + "value": "#FFFFFF" | ||
| 6 | + } | ||
| 7 | + ] | ||
| 8 | +} | ||
| @@ -0,0 +1,5 @@ | |||
| 1 | +{ | ||
| 2 | + "src": [ | ||
| 3 | + "pages/Index" | ||
| 4 | + ] | ||
| 5 | +} | ||
| @@ -0,0 +1 @@ | |||
| 1 | +*.tgz | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | +{ | ||
| 2 | + "modelVersion": "5.0.0", | ||
| 3 | + "dependencies": { | ||
| 4 | + }, | ||
| 5 | + "execution": { | ||
| 6 | + // "analyze": "default", /* Define the build analyze mode. Value: [ "default" | "verbose" | false ]. Default: "default" */ | ||
| 7 | + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ | ||
| 8 | + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ | ||
| 9 | + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ | ||
| 10 | + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ | ||
| 11 | + }, | ||
| 12 | + "logging": { | ||
| 13 | + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ | ||
| 14 | + }, | ||
| 15 | + "debugging": { | ||
| 16 | + "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ | ||
| 17 | + }, | ||
| 18 | + "nodeOptions": { | ||
| 19 | + // "maxOldSpaceSize": 4096 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process */ | ||
| 20 | + } | ||
| 21 | +} | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved | ||
| 3 | + * Use of this source code is governed by a MIT license that can be | ||
| 4 | + * found in the LICENSE file. | ||
| 5 | + */ | ||
| 6 | + | ||
| 7 | +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. | ||
| 8 | +export { appTasks } from '@ohos/hvigor-ohos-plugin'; | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | +{ | ||
| 2 | + "modelVersion": "5.0.0", | ||
| 3 | + "license": "ISC", | ||
| 4 | + "name": "case-demo-datetimepicker-host", | ||
| 5 | + "description": "datetimepicker 用例展示 Demo 宿主", | ||
| 6 | + "repository": {}, | ||
| 7 | + "version": "1.0.0", | ||
| 8 | + "overrides": { | ||
| 9 | + "@rnoh/react-native-openharmony": "file:./../node_modules/@react-native-oh/react-native-harmony/react_native_openharmony.har" | ||
| 10 | + }, | ||
| 11 | + "dynamicDependencies": {} | ||
| 12 | +} | ||
| @@ -0,0 +1,6 @@ | |||
| 1 | +import {AppRegistry} from 'react-native'; | ||
| 2 | +import App from './App'; | ||
| 3 | +import {name as appName} from './app.json'; | ||
| 4 | + | ||
| 5 | +AppRegistry.registerComponent(appName, () => App); | ||
| 6 | + | ||
| @@ -0,0 +1,40 @@ | |||
| 1 | +const path = require('path'); | ||
| 2 | +const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config'); | ||
| 3 | +const {createHarmonyMetroConfig} = require('@react-native-oh/react-native-harmony/metro.config'); | ||
| 4 | + | ||
| 5 | +const projectRoot = __dirname; | ||
| 6 | +// 被测库就是本仓库根目录,源码位于 Demo 目录之外,必须加入监视范围才能被 Metro 解析。 | ||
| 7 | +const repoRoot = path.resolve(projectRoot, '..'); | ||
| 8 | + | ||
| 9 | +module.exports = mergeConfig( | ||
| 10 | + getDefaultConfig(projectRoot), | ||
| 11 | + createHarmonyMetroConfig({ | ||
| 12 | + reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony', | ||
| 13 | + }), | ||
| 14 | + { | ||
| 15 | + watchFolders: [repoRoot], | ||
| 16 | + resolver: { | ||
| 17 | + // 库源码位于仓库根,其 import 需要能解析到本 Demo 的依赖。 | ||
| 18 | + nodeModulesPaths: [path.resolve(projectRoot, 'node_modules')], | ||
| 19 | + // 仓库内其他示例工程各自带依赖与构建产物,不参与本 Demo 的解析。 | ||
| 20 | + blockList: [ | ||
| 21 | + /[\\/]example[\\/].*/, | ||
| 22 | + /[\\/]example_scan[\\/].*/, | ||
| 23 | + /[\\/]bug_demo[\\/].*/, | ||
| 24 | + /[\\/]ohos_graybox[\\/].*/, | ||
| 25 | + /[\\/]outputs[\\/].*/, | ||
| 26 | + // 宿主工程的依赖与构建产物体量大,不参与 JS 解析与文件监视。 | ||
| 27 | + /[\\/]oh_modules[\\/].*/, | ||
| 28 | + /[\\/]harmony[\\/]entry[\\/]build[\\/].*/, | ||
| 29 | + /[\\/]\.cxx[\\/].*/, | ||
| 30 | + /[\\/]\.hvigor[\\/].*/, | ||
| 31 | + ], | ||
| 32 | + }, | ||
| 33 | + maxWorkers: 2, | ||
| 34 | + transformer: { | ||
| 35 | + getTransformOptions: async () => ({ | ||
| 36 | + transform: {experimentalImportSupport: false, inlineRequires: true}, | ||
| 37 | + }), | ||
| 38 | + }, | ||
| 39 | + }, | ||
| 40 | +); | ||
| @@ -0,0 +1,29 @@ | |||
| 1 | +{ | ||
| 2 | + "name": "case-demo-datetimepicker-072", | ||
| 3 | + "version": "1.0.0", | ||
| 4 | + "private": true, | ||
| 5 | + "scripts": { | ||
| 6 | + "start": "react-native start", | ||
| 7 | + "bundle:harmony": "react-native bundle-harmony --dev false", | ||
| 8 | + "typecheck": "tsc --noEmit", | ||
| 9 | + "build:hap": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/build.ps1" | ||
| 10 | + }, | ||
| 11 | + "dependencies": { | ||
| 12 | + "@react-native-community/datetimepicker": "7.6.2", | ||
| 13 | + "@react-native-oh/react-native-harmony": "0.72.139", | ||
| 14 | + "react": "18.2.0", | ||
| 15 | + "react-native": "0.72.5" | ||
| 16 | + }, | ||
| 17 | + "devDependencies": { | ||
| 18 | + "@babel/core": "^7.20.0", | ||
| 19 | + "@babel/runtime": "^7.20.0", | ||
| 20 | + "@react-native/metro-config": "^0.72.11", | ||
| 21 | + "@react-native-oh/react-native-harmony-cli": "0.0.43-1", | ||
| 22 | + "@types/react": "18.2.79", | ||
| 23 | + "metro-react-native-babel-preset": "0.76.8", | ||
| 24 | + "typescript": "5.5.4" | ||
| 25 | + }, | ||
| 26 | + "engines": { | ||
| 27 | + "node": ">=18" | ||
| 28 | + } | ||
| 29 | +} | ||
| @@ -0,0 +1,65 @@ | |||
| 1 | +function Invoke-DemoBuild { | ||
| 2 | + param( | ||
| 3 | + [Parameter(Mandatory)][string]$Root, | ||
| 4 | + [Parameter(Mandatory)][string]$HostFolder, | ||
| 5 | + [Parameter(Mandatory)][string]$OutputName, | ||
| 6 | + [string]$SigningConfig = '', | ||
| 7 | + [Parameter(Mandatory)][scriptblock]$Build | ||
| 8 | + ) | ||
| 9 | + $taskHost = Join-Path $Root $HostFolder | ||
| 10 | + $taskProfilePath = Join-Path $taskHost 'build-profile.json5' | ||
| 11 | + $taskAppPath = Join-Path $taskHost 'AppScope\app.json5' | ||
| 12 | + $taskOriginalProfile = [IO.File]::ReadAllText($taskProfilePath) | ||
| 13 | + $taskOriginalApp = [IO.File]::ReadAllText($taskAppPath) | ||
| 14 | + $taskEncoding = [Text.UTF8Encoding]::new($false) | ||
| 15 | + $taskSigned = -not [string]::IsNullOrWhiteSpace($SigningConfig) | ||
| 16 | + $taskStarted = Get-Date | ||
| 17 | + try { | ||
| 18 | + if ($taskSigned) { | ||
| 19 | + $taskSigningPath = (Resolve-Path -LiteralPath $SigningConfig).Path | ||
| 20 | + $taskSigning = Get-Content -Raw -Encoding UTF8 -LiteralPath $taskSigningPath | ConvertFrom-Json -AsHashtable | ||
| 21 | + if (-not $taskSigning.bundle_name -or -not $taskSigning.signingConfigs) { | ||
| 22 | + throw '签名配置缺少 bundle_name 或 signingConfigs' | ||
| 23 | + } | ||
| 24 | + foreach ($taskEntry in $taskSigning.signingConfigs) { | ||
| 25 | + foreach ($taskKey in @('certpath', 'profile', 'storeFile')) { | ||
| 26 | + $taskMaterialPath = [string]$taskEntry.material[$taskKey] | ||
| 27 | + if (-not [IO.Path]::IsPathRooted($taskMaterialPath)) { | ||
| 28 | + $taskMaterialPath = Join-Path (Split-Path -Parent $taskSigningPath) $taskMaterialPath | ||
| 29 | + } | ||
| 30 | + if (-not (Test-Path -LiteralPath $taskMaterialPath -PathType Leaf)) { | ||
| 31 | + throw "签名材料不存在:$taskKey" | ||
| 32 | + } | ||
| 33 | + $taskEntry.material[$taskKey] = [IO.Path]::GetFullPath($taskMaterialPath) | ||
| 34 | + } | ||
| 35 | + } | ||
| 36 | + $taskProfile = $taskOriginalProfile | ConvertFrom-Json -AsHashtable | ||
| 37 | + $taskApp = $taskOriginalApp | ConvertFrom-Json -AsHashtable | ||
| 38 | + $taskProfile.app.signingConfigs = $taskSigning.signingConfigs | ||
| 39 | + foreach ($taskProduct in $taskProfile.app.products) { | ||
| 40 | + $taskProduct.signingConfig = $taskSigning.signing_config_name | ||
| 41 | + } | ||
| 42 | + $taskApp.app.bundleName = $taskSigning.bundle_name | ||
| 43 | + [IO.File]::WriteAllText($taskProfilePath, ($taskProfile | ConvertTo-Json -Depth 30), $taskEncoding) | ||
| 44 | + [IO.File]::WriteAllText($taskAppPath, ($taskApp | ConvertTo-Json -Depth 30), $taskEncoding) | ||
| 45 | + } | ||
| 46 | + & $Build | ||
| 47 | + $taskOutputDirectory = Join-Path $taskHost 'entry\build\default\outputs\default' | ||
| 48 | + $taskExpected = if ($taskSigned) { 'entry-default-signed.hap' } else { 'entry-default-unsigned.hap' } | ||
| 49 | + $taskHap = Join-Path $taskOutputDirectory $taskExpected | ||
| 50 | + if (-not (Test-Path -LiteralPath $taskHap -PathType Leaf)) { | ||
| 51 | + throw "构建未生成所需 HAP:$taskHap" | ||
| 52 | + } | ||
| 53 | + if ((Get-Item -LiteralPath $taskHap).LastWriteTime -lt $taskStarted) { | ||
| 54 | + throw '找到的 HAP 早于本次构建,不能作为本轮交付' | ||
| 55 | + } | ||
| 56 | + $taskDestination = Join-Path $Root ($OutputName + $(if ($taskSigned) { '-signed.hap' } else { '-unsigned.hap' })) | ||
| 57 | + Copy-Item -LiteralPath $taskHap -Destination $taskDestination -Force | ||
| 58 | + Write-Output "本轮 HAP:$taskDestination" | ||
| 59 | + } finally { | ||
| 60 | + # 签名仅在构建期间生效;共享模板中不保留本机证书与口令。 | ||
| 61 | + [IO.File]::WriteAllText($taskProfilePath, $taskOriginalProfile, $taskEncoding) | ||
| 62 | + [IO.File]::WriteAllText($taskAppPath, $taskOriginalApp, $taskEncoding) | ||
| 63 | + } | ||
| 64 | +} | ||
| 65 | + | ||
| @@ -0,0 +1,43 @@ | |||
| 1 | +param([string]$SigningConfig = '', [switch]$Clean) | ||
| 2 | +$ErrorActionPreference = 'Stop' | ||
| 3 | +$taskRoot = Split-Path -Parent $PSScriptRoot | ||
| 4 | +. (Join-Path $PSScriptRoot 'build-support.ps1') | ||
| 5 | +Push-Location -LiteralPath $taskRoot | ||
| 6 | +try { | ||
| 7 | + if (-not (Test-Path -LiteralPath 'node_modules\react-native\package.json')) { | ||
| 8 | + & npm.cmd install --no-audit --no-fund | ||
【AI-Review】【一般】【基础代码问题】【代码逻辑错误】
● 问题: ● 影响: 文档承诺的 ● 建议: 在 ![]() ![]() | |||
| 9 | + if ($LASTEXITCODE -ne 0) { throw 'npm install 失败' } | ||
| 10 | + } | ||
| 11 | + Invoke-DemoBuild -Root $taskRoot -HostFolder 'harmony' -OutputName 'case_demo_datetimepicker' -SigningConfig $SigningConfig -Build { | ||
| 12 | + Push-Location -LiteralPath (Join-Path $taskRoot 'harmony') | ||
| 13 | + try { | ||
| 14 | + & ohpm install | ||
| 15 | + if ($LASTEXITCODE -ne 0) { throw 'ohpm install 失败' } | ||
| 16 | + if ($Clean) { | ||
| 17 | + & hvigorw clean --no-daemon | ||
| 18 | + if ($LASTEXITCODE -ne 0) { throw '清理旧构建缓存失败' } | ||
| 19 | + } | ||
| 20 | + } finally { | ||
| 21 | + Pop-Location | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + # codegen 必须在 ohpm install 之后:需要从 entry/oh_modules 的 RNOH 包读取组件描述。 | ||
| 25 | + & npx.cmd react-native codegen-harmony --rnoh-module-path '.\harmony\entry\oh_modules\@rnoh\react-native-openharmony' | ||
| 26 | + if ($LASTEXITCODE -ne 0) { throw 'RNOH codegen 失败' } | ||
| 27 | + | ||
| 28 | + & npm.cmd run bundle:harmony | ||
| 29 | + if ($LASTEXITCODE -ne 0) { throw 'JS bundle 失败' } | ||
| 30 | + $taskBundle = Join-Path $taskRoot 'harmony\entry\src\main\resources\rawfile\bundle.harmony.js' | ||
| 31 | + if (-not (Test-Path -LiteralPath $taskBundle)) { throw "未生成 bundle:$taskBundle" } | ||
| 32 | + | ||
| 33 | + Push-Location -LiteralPath (Join-Path $taskRoot 'harmony') | ||
| 34 | + try { | ||
| 35 | + & hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon | ||
| 36 | + if ($LASTEXITCODE -ne 0) { throw 'HarmonyOS 完整 HAP 构建失败' } | ||
| 37 | + } finally { | ||
| 38 | + Pop-Location | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | +} finally { | ||
| 42 | + Pop-Location | ||
| 43 | +} | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | +import type {ComponentType} from 'react'; | ||
| 2 | + | ||
| 3 | +import {DateCompactCase} from './cases/DateCompactCase'; | ||
| 4 | +import {DateSpinnerChangeCase} from './cases/DateSpinnerChangeCase'; | ||
| 5 | +import {DynamicValueCase} from './cases/DynamicValueCase'; | ||
| 6 | +import {TimeIs24HourCase} from './cases/TimeIs24HourCase'; | ||
| 7 | +import type {DemoCaseProps} from './models/testCase'; | ||
| 8 | + | ||
| 9 | +interface DemoCaseEntry { | ||
| 10 | + id: string; | ||
| 11 | + Component: ComponentType<DemoCaseProps>; | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +// 添加用例:导入新的用例组件,再在这里追加一项。 | ||
| 15 | +// 首页自动平铺全部条目;编号统一从这里传给卡片。 | ||
| 16 | +export const demoCases: DemoCaseEntry[] = [ | ||
| 17 | + {id: 'DTP-072-01', Component: DateSpinnerChangeCase}, | ||
| 18 | + {id: 'DTP-072-02', Component: TimeIs24HourCase}, | ||
| 19 | + {id: 'DTP-072-03', Component: DateCompactCase}, | ||
| 20 | + {id: 'DTP-072-04', Component: DynamicValueCase}, | ||
| 21 | +]; | ||
| @@ -0,0 +1,74 @@ | |||
| 1 | +import React, {useState} from 'react'; | ||
| 2 | +import {StyleSheet, Text, View} from 'react-native'; | ||
| 3 | + | ||
| 4 | +import {CaseCard} from '../components/CaseCard'; | ||
| 5 | +import {RNDateTimePicker} from '../datetimepicker'; | ||
| 6 | +import type {CaseContext, CaseDefinition, DemoCaseProps} from '../models/testCase'; | ||
| 7 | +import {PickerFrame, describeDate, readPickerEvent, writeEvent} from './caseShared'; | ||
| 8 | +import type {PickerEventRecord} from './caseShared'; | ||
| 9 | + | ||
| 10 | +const INITIAL_VALUE = new Date(2025, 2, 8, 12, 0); | ||
| 11 | + | ||
| 12 | +export function DateCompactCase({caseId}: DemoCaseProps): React.JSX.Element { | ||
| 13 | + const [value, setValue] = useState(INITIAL_VALUE); | ||
| 14 | + const [events, setEvents] = useState<PickerEventRecord[]>([]); | ||
| 15 | + | ||
| 16 | + function execute(context: CaseContext): void { | ||
| 17 | + context.write('当前 value:' + describeDate(value)); | ||
| 18 | + context.write('已收到 ' + events.length + ' 次 onChange 回调。'); | ||
| 19 | + events.forEach((record, index) => writeEvent(context, index + 1, record)); | ||
| 20 | + | ||
| 21 | + if (events.length === 0) { | ||
| 22 | + context.write('还没有展开选择器并选日期。请先点开 compact 选择器选择一天,再执行。'); | ||
| 23 | + context.decide('未通过'); | ||
| 24 | + return; | ||
| 25 | + } | ||
| 26 | + const ok = events.every( | ||
| 27 | + record => record.type === 'set' && | ||
| 28 | + typeof record.timestamp === 'number' && | ||
| 29 | + Number.isFinite(record.timestamp), | ||
| 30 | + ); | ||
| 31 | + context.decide(ok ? '通过' : '未通过'); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + const testCase: CaseDefinition = { | ||
| 35 | + id: caseId, | ||
| 36 | + title: 'date + compact 选择日期下发 set 事件', | ||
| 37 | + api: 'RNDateTimePicker mode="date" display="compact" onChange', | ||
| 38 | + purpose: '验证 compact 日期选择器展开日历并选中日期后,onChange 带回 set 事件与时间戳。', | ||
| 39 | + steps: '点击下方日期框展开日历,选择另一天,然后点击执行。', | ||
| 40 | + expectedResult: '回调 type 为 set,timestamp 为有限数字,卡片内 value 更新为所选日期。', | ||
| 41 | + failureResult: '点开后无法选择、选择后无回调,或回调缺 timestamp。', | ||
| 42 | + timeoutMs: 15000, | ||
| 43 | + execute, | ||
| 44 | + }; | ||
| 45 | + | ||
| 46 | + return ( | ||
| 47 | + <CaseCard testCase={testCase}> | ||
| 48 | + <View style={styles.block}> | ||
| 49 | + <Text style={styles.label}>compact 日期选择器</Text> | ||
| 50 | + <PickerFrame> | ||
| 51 | + <RNDateTimePicker | ||
| 52 | + mode="date" | ||
| 53 | + display="compact" | ||
| 54 | + value={value} | ||
| 55 | + onChange={(event: any, date?: Date) => { | ||
| 56 | + setEvents(previous => previous.concat(readPickerEvent(event, date))); | ||
| 57 | + if (date instanceof Date) setValue(date); | ||
| 58 | + }} | ||
| 59 | + style={styles.picker} | ||
| 60 | + /> | ||
| 61 | + </PickerFrame> | ||
| 62 | + <Text selectable style={styles.note}>当前 value:{describeDate(value)}</Text> | ||
| 63 | + <Text selectable style={styles.note}>回调次数:{events.length}</Text> | ||
| 64 | + </View> | ||
| 65 | + </CaseCard> | ||
| 66 | + ); | ||
| 67 | +} | ||
| 68 | + | ||
| 69 | +const styles = StyleSheet.create({ | ||
| 70 | + block: {marginBottom: 18}, | ||
| 71 | + label: {fontSize: 15, fontWeight: '600', color: '#24324B', marginBottom: 8}, | ||
| 72 | + picker: {height: 48}, | ||
| 73 | + note: {fontSize: 14, color: '#43516B', marginTop: 8}, | ||
| 74 | +}); | ||
| @@ -0,0 +1,82 @@ | |||
| 1 | +import React, {useState} from 'react'; | ||
| 2 | +import {StyleSheet, Text, View} from 'react-native'; | ||
| 3 | + | ||
| 4 | +import {CaseCard} from '../components/CaseCard'; | ||
| 5 | +import {RNDateTimePicker} from '../datetimepicker'; | ||
| 6 | +import type {CaseContext, CaseDefinition, DemoCaseProps} from '../models/testCase'; | ||
| 7 | +import {PICKER_HEIGHT, PickerFrame, describeDate, readPickerEvent, writeEvent} from './caseShared'; | ||
| 8 | +import type {PickerEventRecord} from './caseShared'; | ||
| 9 | + | ||
| 10 | +const INITIAL_VALUE = new Date(2025, 0, 15, 10, 30); | ||
| 11 | + | ||
| 12 | +export function DateSpinnerChangeCase({caseId}: DemoCaseProps): React.JSX.Element { | ||
| 13 | + const [value, setValue] = useState(INITIAL_VALUE); | ||
| 14 | + const [events, setEvents] = useState<PickerEventRecord[]>([]); | ||
| 15 | + | ||
| 16 | + function onChange(event: any, date?: Date): void { | ||
| 17 | + const record = readPickerEvent(event, date); | ||
| 18 | + setEvents(previous => previous.concat(record)); | ||
| 19 | + if (date instanceof Date) setValue(date); | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + function execute(context: CaseContext): void { | ||
| 23 | + context.write('当前 value:' + describeDate(value)); | ||
| 24 | + context.write('已收到 ' + events.length + ' 次 onChange 回调。'); | ||
| 25 | + events.forEach((record, index) => writeEvent(context, index + 1, record)); | ||
| 26 | + | ||
| 27 | + if (events.length === 0) { | ||
| 28 | + context.write('还没有滚动选择器。请先在卡片内改变日期,再点执行。'); | ||
| 29 | + context.decide('未通过'); | ||
| 30 | + return; | ||
| 31 | + } | ||
| 32 | + const allSet = events.every(record => record.type === 'set'); | ||
| 33 | + const allTimestamped = events.every( | ||
| 34 | + record => typeof record.timestamp === 'number' && Number.isFinite(record.timestamp), | ||
| 35 | + ); | ||
| 36 | + if (!allSet) { | ||
| 37 | + context.write('存在 type 不是 set 的回调;本分支声明仅 set 会下发。'); | ||
| 38 | + } | ||
| 39 | + if (!allTimestamped) { | ||
| 40 | + context.write('存在缺失 nativeEvent.timestamp 的回调。'); | ||
| 41 | + } | ||
| 42 | + context.decide(allSet && allTimestamped ? '通过' : '未通过'); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + const testCase: CaseDefinition = { | ||
| 46 | + id: caseId, | ||
| 47 | + title: 'date + spinner 滚动产生 set 事件', | ||
| 48 | + api: 'RNDateTimePicker mode="date" display="spinner" onChange', | ||
| 49 | + purpose: '验证日期选择器滚动后通过 onChange 下发 type=set 与可用的 timestamp。', | ||
| 50 | + steps: '在下方日期滚轮中选择另一个日期,然后点击执行。', | ||
| 51 | + expectedResult: '执行结果列出至少一次回调,type 为 set,timestamp 为有限数字,第二个参数为 Date。', | ||
| 52 | + failureResult: '滚动后没有任何回调,或回调缺少 timestamp、type 不是 set。', | ||
| 53 | + timeoutMs: 15000, | ||
| 54 | + execute, | ||
| 55 | + }; | ||
| 56 | + | ||
| 57 | + return ( | ||
| 58 | + <CaseCard testCase={testCase}> | ||
| 59 | + <View style={styles.block}> | ||
| 60 | + <Text style={styles.label}>日期选择器(value 会随选择同步)</Text> | ||
| 61 | + <PickerFrame> | ||
| 62 | + <RNDateTimePicker | ||
| 63 | + mode="date" | ||
| 64 | + display="spinner" | ||
| 65 | + value={value} | ||
| 66 | + onChange={onChange} | ||
| 67 | + style={styles.picker} | ||
| 68 | + /> | ||
| 69 | + </PickerFrame> | ||
| 70 | + <Text selectable style={styles.note}>当前 value:{describeDate(value)}</Text> | ||
| 71 | + <Text selectable style={styles.note}>回调次数:{events.length}</Text> | ||
| 72 | + </View> | ||
| 73 | + </CaseCard> | ||
| 74 | + ); | ||
| 75 | +} | ||
| 76 | + | ||
| 77 | +const styles = StyleSheet.create({ | ||
| 78 | + block: {marginBottom: 18}, | ||
| 79 | + label: {fontSize: 15, fontWeight: '600', color: '#24324B', marginBottom: 8}, | ||
| 80 | + picker: {height: PICKER_HEIGHT}, | ||
| 81 | + note: {fontSize: 14, color: '#43516B', marginTop: 8}, | ||
| 82 | +}); | ||
| @@ -0,0 +1,95 @@ | |||
| 1 | +import React, {useState} from 'react'; | ||
| 2 | +import {Pressable, StyleSheet, Text, View} from 'react-native'; | ||
| 3 | + | ||
| 4 | +import {CaseCard} from '../components/CaseCard'; | ||
| 5 | +import {RNDateTimePicker} from '../datetimepicker'; | ||
| 6 | +import type {CaseContext, CaseDefinition, DemoCaseProps} from '../models/testCase'; | ||
| 7 | +import {PICKER_HEIGHT, PickerFrame, describeDate} from './caseShared'; | ||
| 8 | + | ||
| 9 | +const VALUE_A = new Date(2025, 1, 10, 8, 0); | ||
| 10 | +const VALUE_B = new Date(2025, 5, 20, 18, 30); | ||
| 11 | + | ||
| 12 | +export function DynamicValueCase({caseId}: DemoCaseProps): React.JSX.Element { | ||
| 13 | + const [value, setValue] = useState(VALUE_A); | ||
| 14 | + const [switchCount, setSwitchCount] = useState(0); | ||
| 15 | + const [lastTarget, setLastTarget] = useState<'A' | 'B'>('A'); | ||
| 16 | + | ||
| 17 | + function switchValue(target: 'A' | 'B'): void { | ||
| 18 | + setValue(target === 'A' ? VALUE_A : VALUE_B); | ||
| 19 | + setLastTarget(target); | ||
| 20 | + setSwitchCount(previous => previous + 1); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + function execute(context: CaseContext): void { | ||
| 24 | + context.write('切换次数:' + switchCount); | ||
| 25 | + context.write('最后切换目标:' + lastTarget); | ||
| 26 | + context.write('当前 value:' + describeDate(value)); | ||
| 27 | + context.write('期望 value:' + describeDate(lastTarget === 'A' ? VALUE_A : VALUE_B)); | ||
| 28 | + | ||
| 29 | + if (switchCount === 0) { | ||
| 30 | + context.write('还没有切换过 value。请先点“切换到 A/B”各一次并观察选择器,再执行。'); | ||
| 31 | + context.decide('未通过'); | ||
| 32 | + return; | ||
| 33 | + } | ||
| 34 | + const matched = | ||
| 35 | + value.getTime() === (lastTarget === 'A' ? VALUE_A : VALUE_B).getTime(); | ||
| 36 | + if (!matched) { | ||
| 37 | + context.write('JS 侧 value 与最后切换目标不一致。'); | ||
| 38 | + context.decide('未通过'); | ||
| 39 | + return; | ||
| 40 | + } | ||
| 41 | + context.write('请确认切换后滚轮显示是否跟随到对应日期;本属性声明仅在 mode=date 且 display=spinner 下支持动态设置。'); | ||
| 42 | + context.decide(switchCount >= 2 ? '需要人工判断' : '未通过'); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + const testCase: CaseDefinition = { | ||
| 46 | + id: caseId, | ||
| 47 | + title: 'date + spinner 动态更新 value', | ||
| 48 | + api: 'RNDateTimePicker mode="date" display="spinner" value', | ||
| 49 | + purpose: '验证日期选择器在运行时接收新的 value 后更新显示,且不卸载、不换 key。', | ||
| 50 | + steps: '点“切换到 A”和“切换到 B”各一次,观察滚轮日期;再点击执行。', | ||
| 51 | + expectedResult: '每次切换后滚轮显示对应日期(2025-02-10 / 2025-06-20),无崩溃、无白屏。', | ||
| 52 | + failureResult: '切换后滚轮仍显示旧日期、页面崩溃或组件消失。', | ||
| 53 | + timeoutMs: 15000, | ||
| 54 | + execute, | ||
| 55 | + }; | ||
| 56 | + | ||
| 57 | + return ( | ||
| 58 | + <CaseCard testCase={testCase}> | ||
| 59 | + <View style={styles.block}> | ||
| 60 | + <Text style={styles.label}>日期选择器(同一实例切换 value)</Text> | ||
| 61 | + <PickerFrame> | ||
| 62 | + <RNDateTimePicker | ||
| 63 | + mode="date" | ||
| 64 | + display="spinner" | ||
| 65 | + value={value} | ||
| 66 | + onChange={(_event: any, date?: Date) => { | ||
| 67 | + if (date instanceof Date) setValue(date); | ||
| 68 | + }} | ||
| 69 | + style={styles.picker} | ||
| 70 | + /> | ||
| 71 | + </PickerFrame> | ||
| 72 | + <View style={styles.row}> | ||
| 73 | + <Pressable accessibilityRole="button" onPress={() => switchValue('A')} style={styles.button}> | ||
| 74 | + <Text style={styles.buttonText}>切换到 A</Text> | ||
| 75 | + </Pressable> | ||
| 76 | + <Pressable accessibilityRole="button" onPress={() => switchValue('B')} style={styles.button}> | ||
| 77 | + <Text style={styles.buttonText}>切换到 B</Text> | ||
| 78 | + </Pressable> | ||
| 79 | + </View> | ||
| 80 | + <Text selectable style={styles.note}>当前 value:{describeDate(value)}</Text> | ||
| 81 | + <Text selectable style={styles.note}>切换次数:{switchCount}</Text> | ||
| 82 | + </View> | ||
| 83 | + </CaseCard> | ||
| 84 | + ); | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +const styles = StyleSheet.create({ | ||
| 88 | + block: {marginBottom: 18}, | ||
| 89 | + label: {fontSize: 15, fontWeight: '600', color: '#24324B', marginBottom: 8}, | ||
| 90 | + picker: {height: PICKER_HEIGHT}, | ||
| 91 | + row: {flexDirection: 'row', gap: 12, marginTop: 12}, | ||
| 92 | + button: {backgroundColor: '#E8EDF7', paddingHorizontal: 16, paddingVertical: 10, borderRadius: 8}, | ||
| 93 | + buttonText: {color: '#24324B', fontSize: 15, fontWeight: '600'}, | ||
| 94 | + note: {fontSize: 14, color: '#43516B', marginTop: 8}, | ||
| 95 | +}); | ||
| @@ -0,0 +1,99 @@ | |||
| 1 | +import React, {useState} from 'react'; | ||
| 2 | +import {StyleSheet, Text, View} from 'react-native'; | ||
| 3 | + | ||
| 4 | +import {CaseCard} from '../components/CaseCard'; | ||
| 5 | +import {RNDateTimePicker} from '../datetimepicker'; | ||
| 6 | +import type {CaseContext, CaseDefinition, DemoCaseProps} from '../models/testCase'; | ||
| 7 | +import {PICKER_HEIGHT, PickerFrame, readPickerEvent} from './caseShared'; | ||
| 8 | +import type {PickerEventRecord} from './caseShared'; | ||
| 9 | + | ||
| 10 | +const INITIAL_VALUE = new Date(2025, 0, 15, 9, 5); | ||
| 11 | + | ||
| 12 | +export function TimeIs24HourCase({caseId}: DemoCaseProps): React.JSX.Element { | ||
| 13 | + const [value24, setValue24] = useState(INITIAL_VALUE); | ||
| 14 | + const [value12, setValue12] = useState(INITIAL_VALUE); | ||
| 15 | + const [events24, setEvents24] = useState<PickerEventRecord[]>([]); | ||
| 16 | + const [events12, setEvents12] = useState<PickerEventRecord[]>([]); | ||
| 17 | + | ||
| 18 | + function execute(context: CaseContext): void { | ||
| 19 | + context.write('is24Hour=true 回调次数:' + events24.length); | ||
| 20 | + events24.forEach((record, index) => context.write(' #' + (index + 1) + ' type=' + record.type + ' timestamp=' + String(record.timestamp))); | ||
| 21 | + context.write('is24Hour=false 回调次数:' + events12.length); | ||
| 22 | + events12.forEach((record, index) => context.write(' #' + (index + 1) + ' type=' + record.type + ' timestamp=' + String(record.timestamp))); | ||
| 23 | + context.write( | ||
| 24 | + '两个滚轮都应正常显示;24 小时制为 0-23,12 小时制带上午/下午分段。' + | ||
| 25 | + '请对照屏幕确认后给出结论。', | ||
| 26 | + ); | ||
| 27 | + | ||
| 28 | + const has24 = events24.length > 0; | ||
| 29 | + const has12 = events12.length > 0; | ||
| 30 | + context.write( | ||
| 31 | + '回调覆盖:is24Hour=true ' + (has24 ? '已产生' : '未产生') + | ||
| 32 | + ';is24Hour=false ' + (has12 ? '已产生' : '未产生') + '。', | ||
| 33 | + ); | ||
| 34 | + if (!has24 && !has12) { | ||
| 35 | + context.write('两个时间滚轮都还没有产生回调。请分别滚动后再执行。'); | ||
| 36 | + context.decide('未通过'); | ||
| 37 | + return; | ||
| 38 | + } | ||
| 39 | + if (!has24 || !has12) { | ||
| 40 | + context.write('只有一个滚轮产生回调,无法比较两种制式。'); | ||
| 41 | + context.decide('未通过'); | ||
| 42 | + return; | ||
| 43 | + } | ||
| 44 | + context.decide('需要人工判断'); | ||
| 45 | + } | ||
| 46 | + | ||
| 47 | + const testCase: CaseDefinition = { | ||
| 48 | + id: caseId, | ||
| 49 | + title: 'time 模式 is24Hour 两种制式渲染', | ||
| 50 | + api: 'RNDateTimePicker mode="time" display="spinner" is24Hour', | ||
| 51 | + purpose: '验证时间选择器按 is24Hour 切换 24 小时制与 12 小时制,且选择后事件正常下发。', | ||
| 52 | + steps: '分别滚动上方 24 小时制与下方 12 小时制时间滚轮,确认显示制式后点击执行。', | ||
| 53 | + expectedResult: '两个时间滚轮都能显示并滚动;24 小时制无上午/下午分段,12 小时制有分段;交互产生 set 事件。', | ||
| 54 | + failureResult: '任一滚轮不显示、制式不随 is24Hour 变化,或交互没有回调。', | ||
| 55 | + timeoutMs: 15000, | ||
| 56 | + execute, | ||
| 57 | + }; | ||
| 58 | + | ||
| 59 | + return ( | ||
| 60 | + <CaseCard testCase={testCase}> | ||
| 61 | + <View style={styles.block}> | ||
| 62 | + <Text style={styles.label}>is24Hour = true</Text> | ||
| 63 | + <PickerFrame> | ||
| 64 | + <RNDateTimePicker | ||
| 65 | + mode="time" | ||
| 66 | + display="spinner" | ||
| 67 | + is24Hour | ||
| 68 | + value={value24} | ||
| 69 | + onChange={(event: any, date?: Date) => { | ||
| 70 | + setEvents24(previous => previous.concat(readPickerEvent(event, date))); | ||
| 71 | + if (date instanceof Date) setValue24(date); | ||
| 72 | + }} | ||
| 73 | + style={styles.picker} | ||
| 74 | + /> | ||
| 75 | + </PickerFrame> | ||
| 76 | + <Text style={styles.label}>is24Hour = false</Text> | ||
| 77 | + <PickerFrame> | ||
| 78 | + <RNDateTimePicker | ||
| 79 | + mode="time" | ||
| 80 | + display="spinner" | ||
| 81 | + is24Hour={false} | ||
| 82 | + value={value12} | ||
| 83 | + onChange={(event: any, date?: Date) => { | ||
| 84 | + setEvents12(previous => previous.concat(readPickerEvent(event, date))); | ||
| 85 | + if (date instanceof Date) setValue12(date); | ||
| 86 | + }} | ||
| 87 | + style={styles.picker} | ||
| 88 | + /> | ||
| 89 | + </PickerFrame> | ||
| 90 | + </View> | ||
| 91 | + </CaseCard> | ||
| 92 | + ); | ||
| 93 | +} | ||
| 94 | + | ||
| 95 | +const styles = StyleSheet.create({ | ||
| 96 | + block: {marginBottom: 18}, | ||
| 97 | + label: {fontSize: 15, fontWeight: '600', color: '#24324B', marginBottom: 8, marginTop: 10}, | ||
| 98 | + picker: {height: PICKER_HEIGHT}, | ||
| 99 | +}); | ||
| @@ -0,0 +1,132 @@ | |||
| 1 | +import React, {useEffect, useRef, useState} from 'react'; | ||
| 2 | +import type {ReactNode} from 'react'; | ||
| 3 | +import {Pressable, StyleSheet, Text, View} from 'react-native'; | ||
| 4 | + | ||
| 5 | +import type { | ||
| 6 | + CaseVerdict, | ||
| 7 | + CaseDefinition, | ||
| 8 | + CaseContext, | ||
| 9 | +} from '../models/testCase'; | ||
| 10 | + | ||
| 11 | +export function CaseCard({testCase, children}: {testCase: CaseDefinition; children?: ReactNode}): React.JSX.Element { | ||
| 12 | + const [observations, setObservations] = useState<string[]>([]); | ||
| 13 | + const [verdict, setVerdict] = useState<CaseVerdict | ''>(''); | ||
| 14 | + const [status, setStatus] = useState('未执行'); | ||
| 15 | + const [running, setRunning] = useState(false); | ||
| 16 | + const attemptRef = useRef(0); | ||
| 17 | + const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
| 18 | + | ||
| 19 | + useEffect(() => () => { | ||
| 20 | + attemptRef.current += 1; | ||
| 21 | + if (timerRef.current !== null) clearTimeout(timerRef.current); | ||
| 22 | + }, []); | ||
| 23 | + | ||
| 24 | + async function execute(): Promise<void> { | ||
| 25 | + const attempt = ++attemptRef.current; | ||
| 26 | + if (timerRef.current !== null) clearTimeout(timerRef.current); | ||
| 27 | + setObservations([]); | ||
| 28 | + setVerdict(''); | ||
| 29 | + setRunning(true); | ||
| 30 | + setStatus('正在执行 ' + testCase.id); | ||
| 31 | + | ||
| 32 | + const context: CaseContext = { | ||
| 33 | + write: text => { | ||
| 34 | + if (attempt === attemptRef.current) { | ||
| 35 | + setObservations(previous => previous.concat(text)); | ||
| 36 | + } | ||
| 37 | + }, | ||
| 38 | + decide: nextVerdict => { | ||
| 39 | + if (attempt === attemptRef.current) setVerdict(nextVerdict); | ||
| 40 | + }, | ||
| 41 | + }; | ||
| 42 | + | ||
| 43 | + // 在调用前留下编号。等待一帧只用于显示,不插入到被测动作之间。 | ||
| 44 | + await new Promise<void>(resolve => requestAnimationFrame(() => resolve())); | ||
| 45 | + if (attempt !== attemptRef.current) return; | ||
| 46 | + const timeoutMs = testCase.timeoutMs ?? 8000; | ||
| 47 | + timerRef.current = setTimeout(() => { | ||
| 48 | + if (attempt !== attemptRef.current) return; | ||
| 49 | + setRunning(false); | ||
| 50 | + setVerdict('需要人工判断'); | ||
| 51 | + setStatus('等待超时;底层任务未被模板自动取消'); | ||
| 52 | + context.write( | ||
| 53 | + '在指定时间内没有返回或收到回调(' + timeoutMs / 1000 + ' 秒)。', | ||
| 54 | + ); | ||
| 55 | + }, timeoutMs); | ||
| 56 | + | ||
| 57 | + try { | ||
| 58 | + await testCase.execute(context); | ||
| 59 | + if (attempt === attemptRef.current) { | ||
| 60 | + setStatus('执行结束'); | ||
| 61 | + setVerdict(previous => previous || '需要人工判断'); | ||
| 62 | + } | ||
| 63 | + } finally { | ||
| 64 | + // 仅回收界面状态。没有 catch,意外异常继续交给 React Native 暴露。 | ||
| 65 | + if (attempt === attemptRef.current) { | ||
| 66 | + if (timerRef.current !== null) clearTimeout(timerRef.current); | ||
| 67 | + setRunning(false); | ||
| 68 | + } | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + return ( | ||
| 73 | + <View style={styles.card} testID={'case_' + testCase.id}> | ||
| 74 | + <Text selectable style={styles.caseId}>{testCase.id}</Text> | ||
| 75 | + <Text style={styles.title}>{testCase.title}</Text> | ||
| 76 | + {testCase.api && <Field label="测试接口" text={testCase.api} />} | ||
| 77 | + <Field label="测试目的" text={testCase.purpose} /> | ||
| 78 | + <Field label="操作步骤" text={testCase.steps} /> | ||
| 79 | + <Field label="预期结果" text={testCase.expectedResult} /> | ||
| 80 | + {testCase.failureResult && <Field label="异常表现" text={testCase.failureResult} />} | ||
| 81 | + {children} | ||
| 82 | + {testCase.crashRisk && ( | ||
| 83 | + <Text style={styles.risk}> | ||
| 84 | + 本用例单独执行。如果应用退出或页面消失,请记录当前用例编号并人工判断。 | ||
| 85 | + </Text> | ||
| 86 | + )} | ||
| 87 | + <Pressable | ||
| 88 | + accessibilityRole="button" | ||
| 89 | + accessibilityLabel={'执行用例 ' + testCase.id} | ||
| 90 | + disabled={running} | ||
| 91 | + onPress={() => { void execute(); }} | ||
| 92 | + style={[styles.button, running && styles.buttonRunning]}> | ||
| 93 | + <Text style={styles.buttonText}>{running ? '执行中' : '执行用例'}</Text> | ||
| 94 | + </Pressable> | ||
| 95 | + <Text style={styles.status}>{status}</Text> | ||
| 96 | + <View style={styles.divider} /> | ||
| 97 | + <Text style={styles.label}>实际结果</Text> | ||
| 98 | + <View style={styles.output}> | ||
| 99 | + <Text selectable style={styles.outputText}>{observations.join('\n\n')}</Text> | ||
| 100 | + </View> | ||
| 101 | + <Text style={styles.label}>执行结论:{verdict}</Text> | ||
| 102 | + </View> | ||
| 103 | + ); | ||
| 104 | +} | ||
| 105 | + | ||
| 106 | +function Field({label, text}: {label: string; text: string}): React.JSX.Element { | ||
| 107 | + return ( | ||
| 108 | + <View style={styles.field}> | ||
| 109 | + <Text style={styles.label}>{label}</Text> | ||
| 110 | + <Text selectable style={styles.body}>{text}</Text> | ||
| 111 | + </View> | ||
| 112 | + ); | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +const styles = StyleSheet.create({ | ||
| 116 | + card: {padding: 20, marginBottom: 20, borderRadius: 16, backgroundColor: '#FFFFFF'}, | ||
| 117 | + caseId: {fontSize: 13, fontWeight: '600', color: '#315BE8', marginBottom: 8}, | ||
| 118 | + title: {fontSize: 20, fontWeight: '700', color: '#1F2B48', marginBottom: 20}, | ||
| 119 | + field: {marginBottom: 16}, | ||
| 120 | + label: {fontSize: 15, fontWeight: '600', color: '#24324B', marginBottom: 6}, | ||
| 121 | + body: {fontSize: 15, color: '#43516B', lineHeight: 23}, | ||
| 122 | + button: {alignSelf: 'flex-start', backgroundColor: '#315BE8', paddingHorizontal: 22, paddingVertical: 12, borderRadius: 10}, | ||
| 123 | + buttonRunning: {opacity: 0.6}, | ||
| 124 | + buttonText: {color: '#FFFFFF', fontSize: 16, fontWeight: '600'}, | ||
| 125 | + status: {fontSize: 13, color: '#66748C', marginTop: 12}, | ||
| 126 | + divider: {height: 1, backgroundColor: '#E3E8F0', marginVertical: 18}, | ||
| 127 | + output: {minHeight: 64, padding: 12, marginBottom: 16, borderRadius: 8, backgroundColor: '#F0F3F8'}, | ||
| 128 | + outputText: {color: '#24324B', fontSize: 14, lineHeight: 22}, | ||
| 129 | + risk: {color: '#B45117', marginBottom: 16, lineHeight: 22}, | ||
| 130 | +}); | ||
| 131 | + | ||
| 132 | + | ||
| @@ -0,0 +1,11 @@ | |||
| 1 | +import type {ComponentType} from 'react'; | ||
| 2 | + | ||
| 3 | +// 直接引用仓库根目录的 JS 实现,Demo 内不重复安装被测库; | ||
| 4 | +// 用例观察到的就是当前分支的 src/index.js。 | ||
| 5 | +const DateTimePickerModule: { | ||
| 6 | + default: ComponentType<any>; | ||
| 7 | + DateTimePickerAndroid: any; | ||
| 8 | +} = require('../../src/index.js'); | ||
| 9 | + | ||
| 10 | +export const RNDateTimePicker: ComponentType<any> = DateTimePickerModule.default; | ||
| 11 | +export const DateTimePickerAndroid: any = DateTimePickerModule.DateTimePickerAndroid; | ||
| @@ -0,0 +1,24 @@ | |||
| 1 | +export type CaseVerdict = '通过' | '未通过' | '需要人工判断'; | ||
| 2 | + | ||
| 3 | +export interface CaseContext { | ||
| 4 | + write: (text: string) => void; | ||
| 5 | + decide: (verdict: CaseVerdict) => void; | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +export interface CaseDefinition { | ||
| 9 | + id: string; | ||
| 10 | + title: string; | ||
| 11 | + purpose: string; | ||
| 12 | + steps: string; | ||
| 13 | + expectedResult: string; | ||
| 14 | + failureResult?: string; | ||
| 15 | + api?: string; | ||
| 16 | + execute: (context: CaseContext) => void | Promise<void>; | ||
| 17 | + timeoutMs?: number; | ||
| 18 | + crashRisk?: boolean; | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | +export interface DemoCaseProps { | ||
| 22 | + caseId: string; | ||
| 23 | +} | ||
| 24 | + | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +import React from 'react'; | ||
| 2 | +import {ScrollView, StyleSheet, Text, View} from 'react-native'; | ||
| 3 | + | ||
| 4 | +import {demoCases} from '../caseRegistry'; | ||
| 5 | + | ||
| 6 | +export function HomePage(): React.JSX.Element { | ||
| 7 | + return ( | ||
| 8 | + <ScrollView contentContainerStyle={styles.container} keyboardShouldPersistTaps="handled"> | ||
| 9 | + <View style={styles.content}> | ||
| 10 | + <Text style={styles.title}>DateTimePicker 用例展示 Demo</Text> | ||
| 11 | + <Text style={styles.note}>br_rnoh0.72 · 共 {demoCases.length} 条用例</Text> | ||
| 12 | + {demoCases.map(entry => ( | ||
| 13 | + <entry.Component key={entry.id} caseId={entry.id} /> | ||
| 14 | + ))} | ||
| 15 | + {demoCases.length === 0 && <Text style={styles.note}>在用例列表中添加第一张卡片。</Text>} | ||
| 16 | + </View> | ||
| 17 | + </ScrollView> | ||
| 18 | + ); | ||
| 19 | +} | ||
| 20 | + | ||
| 21 | +const styles = StyleSheet.create({ | ||
| 22 | + container: {padding: 20, alignItems: 'center'}, | ||
| 23 | + content: {width: '100%', maxWidth: 920}, | ||
| 24 | + title: {fontSize: 26, fontWeight: '700', color: '#1F2B48', marginBottom: 8}, | ||
| 25 | + note: {fontSize: 14, color: '#66748C', lineHeight: 22, marginBottom: 20}, | ||
| 26 | +}); | ||
| @@ -0,0 +1,27 @@ | |||
| 1 | +{ | ||
| 2 | + "compilerOptions": { | ||
| 3 | + "target": "ES2020", | ||
| 4 | + "module": "ESNext", | ||
| 5 | + "moduleResolution": "node", | ||
| 6 | + "jsx": "react-native", | ||
| 7 | + "strict": true, | ||
| 8 | + "esModuleInterop": true, | ||
| 9 | + "allowSyntheticDefaultImports": true, | ||
| 10 | + "resolveJsonModule": true, | ||
| 11 | + "skipLibCheck": true, | ||
| 12 | + "noEmit": true, | ||
| 13 | + "types": [ | ||
| 14 | + "react" | ||
| 15 | + ] | ||
| 16 | + }, | ||
| 17 | + "include": [ | ||
| 18 | + "App.tsx", | ||
| 19 | + "src/**/*.ts", | ||
| 20 | + "src/**/*.tsx" | ||
| 21 | + ], | ||
| 22 | + "exclude": [ | ||
| 23 | + "node_modules", | ||
| 24 | + "harmony" | ||
| 25 | + ] | ||
| 26 | +} | ||
| 27 | + | ||


【AI-Review】【建议】【软件设计】【冗余重复代码】 ● 问题:
case_demo/harmony/codelinter.json:5与case_demo/harmony/codelinter.json:7均为"**/node_modules/**/*",ignore 数组中同一规则重复出现两次。● 影响: 无功能影响,但重复项增加阅读噪音,后续维护 ignore 列表时易误判已覆盖范围。
● 建议: 删除第 7 行重复的
"**/node_modules/**/*"。