已开启
test(case-demo): add datetimepicker case demo for br_rnoh0.72 #63
test(case-demo): add datetimepicker case demo for br_rnoh0.72 #63
已开启
Kerany创建于 18 小时前
共 49 个文件变更+12127-0
Acase_demo/.gitignore+19-0文件内容审核中,请稍后刷新重试
@@ -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+ 
Acase_demo/harmony/.gitignore+22-0文件内容审核中,请稍后刷新重试
@@ -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+}
Acase_demo/harmony/AppScope/resources/base/element/string.json+8-0文件内容审核中,请稍后刷新重试
Acase_demo/harmony/build-profile.json5+53-0文件内容审核中,请稍后刷新重试
@@ -0,0 +1,32 @@
1+{
2+ "files": ["**/*.ts", "**/*.ets"],
3+ "ignore": [
4+ "**/ohosTest/**/*",
5+ "**/node_modules/**/*",
6+ "**/hvigorfile.ts",
7+ "**/node_modules/**/*",
cpf-manager
cpf-managercpf-manager18 小时前

【AI-Review】【建议】【软件设计】【冗余重复代码】 ● 问题: case_demo/harmony/codelinter.json:5 与 case_demo/harmony/codelinter.json:7 均为 "**/node_modules/**/*",ignore 数组中同一规则重复出现两次。

● 影响: 无功能影响,但重复项增加阅读噪音,后续维护 ignore 列表时易误判已覆盖范围。

● 建议: 删除第 7 行重复的 "**/node_modules/**/*"。

likedislike
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
cpf-manager
cpf-managercpf-manager18 小时前

【AI-Review】【建议】【编码规范】【注释/可读性】 ● 问题: case_demo/harmony/entry/src/main/ets/pages/Index.ets:89-90 的注释为"to load the bundle from file, place it in /data/app/el2/100/base/com.rnoh.tester/files/bundle.harmony.js on your device",其中 com.rnoh.tester 是 RNOH tester 工程的 bundleName,与本 Demo 的 com.example.datetimepicker.casedemo072(case_demo/harmony/AppScope/app.json5:3)不一致。该注释是从 example 模板复制而来,未按本 Demo 的实际包名更新。

● 影响: 若使用者按注释把 bundle 推到 /data/app/el2/100/base/com.rnoh.tester/...,将找不到文件,FileJSBundleProvider('/data/storage/el2/base/files/bundle.harmony.js')(第 91 行)的实际路径与注释也不对应,误导调试。

● 建议: 将注释中的示例路径更新为本 Demo 沙箱对应的路径,或直接删除该 NOTE,避免与实际 FileJSBundleProvider 参数脱节。

likedislike
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+}
Acase_demo/harmony/entry/src/main/resources/base/element/string.json+16-0文件内容审核中,请稍后刷新重试
@@ -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
cpf-manager
cpf-managercpf-manager18 小时前

【AI-Review】【一般】【基础代码问题】【代码逻辑错误】 ● 问题: case_demo/scripts/build.ps1:8 的自动安装命令为 npm.cmd install --no-audit --no-fund,未带 --legacy-peer-deps,而同仓库 example/package.json 的全部安装脚本(i/install:pkg/fast:pkg)与本 PR case_demo/README.md:63 的手动步骤均使用 npm install --legacy-peer-deps。在从零拉取(node_modules 不存在)后执行 pwsh -File scripts/build.ps1 时,@react-native-oh/react-native-harmony@0.72.139 与 react-native@0.72.5 等依赖存在 peer 冲突,npm 默认会抛出 ERESOLVE could not resolve 并中断,使该脚本与 README 共同声明的"一键构建"路径无法跑通。

● 影响: 文档承诺的 pwsh -File scripts/build.ps1 自动构建在首次执行时即失败;使用者必须回退到 README 的手动步骤才能完成安装,构建脚本未达到其封装目的。

● 建议: 在 build.ps1:8 的 npm.cmd install 后追加 --legacy-peer-deps,与 README 及 example 实践保持一致。

likedislike
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,47 @@
1+import React from 'react';
2+import {StyleSheet, View} from 'react-native';
3+import type {ReactNode} from 'react';
4+ 
5+import type {CaseContext} from '../models/testCase';
6+ 
7+export const PICKER_HEIGHT = 180;
8+ 
9+export function PickerFrame({children}: {children: ReactNode}): React.JSX.Element {
10+ return <View style={styles.frame}>{children}</View>;
11+}
12+ 
13+export type PickerEventRecord = {
14+ type: string;
15+ timestamp: number | undefined;
16+ utcOffset: unknown;
17+ dateText: string;
18+};
19+ 
20+export function readPickerEvent(event: any, date?: Date): PickerEventRecord {
21+ const nativeEvent = event?.nativeEvent ?? {};
22+ const timestamp = nativeEvent.timestamp;
23+ return {
24+ type: String(event?.type),
25+ timestamp: typeof timestamp === 'number' ? timestamp : undefined,
26+ utcOffset: nativeEvent.utcOffset,
27+ dateText: date instanceof Date ? date.toLocaleString() : '未返回 Date',
28+ };
29+}
30+ 
31+export function writeEvent(context: CaseContext, index: number, record: PickerEventRecord): void {
32+ context.write(
33+ '第 ' + index + ' 次回调:\n' +
34+ 'event.type = ' + record.type + '\n' +
35+ 'nativeEvent.timestamp = ' + String(record.timestamp) + '\n' +
36+ 'nativeEvent.utcOffset = ' + String(record.utcOffset) + '\n' +
37+ '第二个参数 = ' + record.dateText,
38+ );
39+}
40+ 
41+export function describeDate(value: Date): string {
42+ return value.toLocaleString();
43+}
44+ 
45+const styles = StyleSheet.create({
46+ frame: {borderWidth: 1, borderColor: '#D7DEE9', borderRadius: 12, paddingVertical: 4, backgroundColor: '#FBFCFE'},
47+});
@@ -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+