灯光模拟HarmonyOS应用实战-62-保证近光为何还要再点一次-普通灯光与实操超时分支对齐

在灯光模拟训练中,“执行一次动作”和“维持某个状态”看起来很接近,落到判题代码里却可能产生完全不同的结果。

这篇文章面向维护 HarmonyOS/ArkTS 训练类状态机的开发者;分析依据是本地 The_kemusan 工程中 QuestionBank.etsIndex.ets 的静态分支,目标是把判题差异压缩成一个可审计的最小改动。

当前题库中的 p_meet 指令写的是“夜间与机动车会车:保证近光灯状态”。如果上一条指令结束后,灯光本来就处于近光状态,学员看到这句话时,很自然地会认为当前状态已经符合要求,不需要再点击一次“近光灯”。但从源码分支看,同样是 ACTION_LOW + LIGHT_LOW + 等待超时,普通灯光模式和实操模式会走向不同结果:

  • 普通灯光模式的 handleTimeout 会在超时点再次查看灯态;若目标动作是 ACTION_LOW,当前灯态也已经是 LIGHT_LOW,则按正确结果收口。
  • 实操模式的 handlePracticalTimeout 只要当前指令仍未完成,就按失败结果收口,没有为 p_meet 保留已经满足近光状态的分支。

这篇文章只处理这一个差异:保留普通模式现有行为,把 p_meet 的实操超时结果与之对齐,并用差异回归锁住边界。文中的修改代码都是建议方案,尚未写入项目源码。

普通灯光与实操超时分支对齐

读完本文,你会得到四个可以直接用于代码评审和回归测试的结果:

  • 用同一条时间序列解释普通模式与实操模式为何出现不同判定。
  • 把“已有近光能否满足指令”收敛为一个没有页面副作用的纯判断。
  • 用差异矩阵锁定唯一允许变化的 p_meet + LIGHT_LOW 业务格子。
  • 分开记录源码事实、建议实现、构建结果与设备操作,避免把静态分析写成已经落地。

一、先把问题压缩成一个可重复的时间序列

这个问题不是“近光按钮能不能点击”,也不是“灯态能不能切换”。真正需要比较的是:两个训练入口在相同输入条件下,超时回调给出了不同结论。

可以把场景压缩为下面五步:

时间点普通灯光模式实操模式
T0上一条操作结束上一条操作结束
T1当前灯态为 LIGHT_LOW当前灯态为 LIGHT_LOW
T2新指令期望 ACTION_LOW新指令为 p_meet,动作字段也是 ACTION_LOW
T3学员没有再次点击近光灯学员没有再次点击近光灯
T4handleTimeout 查看现有灯态并可判正确handlePracticalTimeout 直接进入失败收口

这组输入的核心不是“学员什么都没有做”,而是“指令出现时,目标状态已经成立”。如果一条指令强调“保证近光灯状态”,判题时就必须回答一个明确的问题:

这条指令要求产生一次新的近光动作,还是要求超时前保持近光状态?

从当前普通模式的超时逻辑看,项目已经存在“现有状态可以满足 ACTION_LOW”这一行为。差异产生在实操超时分支没有复用它。

为了避免讨论被页面样式、按钮动画、语音播放和历史记录分散,本文只观察三个输入和一个输出:

interface TimeoutObservation {
  expectedAction: string
  currentLightState: string
  commandId: string
  passed: boolean
}

这段结构不是要加入业务模型,而是帮助我们限定分析范围。expectedActioncurrentLightStatecommandId 足以描述本次分支差异,passed 是需要对齐的结果。页面布局、计时器展示和语音内容都不参与这个最小判断。

二、从 PracticalCommand 与 p_meet 确认源码事实

实操指令来自 QuestionBank.ets。当前 PracticalCommand 主要携带编号、文本、动作和分类,p_meet 使用 ACTION_LOW 表示近光要求。

下面是只保留本文相关字段的等价摘录,字段含义与当前源码一致,排版为了讲解做了收缩,并非逐字复制源码:

export interface PracticalCommand {
  id: string
  text: string
  action: string
  category: string
}

export const PRACTICAL_COMMANDS: PracticalCommand[] = [
  {
    id: 'p_meet',
    text: '夜间与机动车会车:保证近光灯状态',
    action: ACTION_LOW,
    category: '会车'
  }
]

这里有两个可以直接从源码确认的事实。

第一,p_meet 的文案包含“保证近光灯状态”。它描述的是会车场景应当处于什么灯态,并没有在文案中要求“必须重新点击近光灯按钮”。

第二,它的数据模型仍然把这条要求放在 action 字段里,值为 ACTION_LOW。因此,判题代码如果只比较动作事件,就会把“需要近光”解释成“需要再发生一次近光点击”;如果在超时点同时读取灯态,则可能把它解释成“当前近光已经满足”。

源码事实与本文建议需要分开看:

内容当前可以确认的事实本文不直接断言的内容
指令数据p_meetactionACTION_LOW产品设计者最初是否有意要求重复点击
指令文案文案强调“保证近光灯状态”所有含 ACTION_LOW 的指令都应接受已有状态
普通超时ACTION_LOW + LIGHT_LOW 存在正确分支该分支是否覆盖了全部业务场景
实操超时当前没有对应的状态满足分支真机上已经有人因此失败
本文方案只建议让 p_meet 复用现有状态语义尚未修改源码,也没有执行运行验证

最后一列很重要。源码结构可以说明“某个输入会进入哪个分支”,却不能替代真实设备操作记录。本文没有把静态阅读推断写成已经发生的用户故障。

三、两条超时路径的差异究竟在哪里

普通灯光与实操模式超时流程

普通灯光模式的 handleTimeout 并非一律把超时判为失败。它先确认回调仍对应当前训练,再为近光状态保留一个补偿判断。以下代码只表达决定性条件,是结构等价的收缩版,不是对源码逐字复制:

private handleTimeout(expectedAction: string): void {
  if (!this.isExamActive) {
    return
  }

  if (expectedAction === ACTION_LOW &&
      this.currentLightState === LIGHT_LOW) {
    // 普通模式:目标为近光,而且超时点已经处于近光。
    this.completeCurrentCommand(true)
    return
  }

  this.completeCurrentCommand(false)
}

真正决定本文结论的是第二个 if:即使等待期间没有收到新的近光点击,只要超时点满足 ACTION_LOW + LIGHT_LOW,普通模式仍可按正确结果结束当前指令。

实操模式的 handlePracticalTimeout 也会先处理活动状态和当前指令等保护条件,但在这些保护条件之后,没有与普通模式对应的灯态判断。它的关键结构可以压缩为下面的等价伪代码,同样不是逐字源码:

private handlePracticalTimeout(commandId: string): void {
  if (!this.isPracticalExamActive) {
    return
  }

  if (this.currentPracticalCommand?.id !== commandId) {
    return
  }

  // 实操模式:当前指令仍在等待时,直接按失败收口。
  this.completeCurrentPracticalCommand(false)
}

因此,差异不在 ACTION_LOW 常量,也不在 LIGHT_LOW 的表示方式,而在两个超时回调的最后一段:

  • 普通分支:先问“目标状态是否已经满足”,再决定失败。
  • 实操分支:保护条件通过后,直接失败。
  • p_meet:恰好把状态语义写在文案里,却进入了没有状态补偿的实操分支。

还要注意,超时函数通常不仅负责返回布尔值。它还可能清理定时器、更新提示语、增加得分、写入记录或切换下一条命令。因此,修复时不宜复制整段普通模式代码到实操模式。复制越多,两个入口以后越容易再次分叉。

更合适的边界是只抽取“超时点是否已满足近光”这一小块纯判断,原有收口流程仍由各自入口负责。

四、先用特征测试固定当前分支差异

在改变结果之前,先写一个能描述现状的特征测试。它的作用不是证明哪个结果更合理,而是让维护者清楚看到:当前两个入口对同一组关键输入确实给出了不同输出。

如果项目已经有可调用的超时测试入口,可以直接驱动页面状态;如果页面状态不方便构造,也可以先通过很薄的适配层暴露结果。下面用 runOrdinaryTimeoutrunPracticalTimeout 表示这两个入口,它们是测试设计示例,并非项目现有函数:

describe('当前超时分支特征', () => {
  it('普通模式在 ACTION_LOW 与 LIGHT_LOW 同时成立时通过', () => {
    const result = runOrdinaryTimeout({
      expectedAction: ACTION_LOW,
      currentLightState: LIGHT_LOW
    })

    expect(result.passed).assertTrue()
  })

  it('实操 p_meet 在相同灯态下仍然失败', () => {
    const result = runPracticalTimeout({
      commandId: 'p_meet',
      expectedAction: ACTION_LOW,
      currentLightState: LIGHT_LOW
    })

    expect(result.passed).assertFalse()
  })
})

这两条测试都应当描述修改前的行为。第一条防止后续修复误伤普通模式;第二条把待对齐的差异显式记录下来。

如果直接测试页面方法,计时器会让用例变慢,也容易受到异步顺序影响。更稳妥的做法是把“定时器何时触发”和“触发后怎样判定”分开。测试只调用判定入口,不需要真的等待若干秒。

特征测试还应记录终态提交次数。一个超时回调和一个点击事件可能在相邻时间片内到达,如果两个路径都能结束当前指令,就需要防止重复加分或重复写入历史。

it('点击收口后到达的超时回调不会再次提交结果', () => {
  const session = createPracticalSession('p_meet', LIGHT_LOW)

  session.handleAction(ACTION_LOW)
  session.handleTimeout('p_meet')

  expect(session.resultCommitCount).assertEqual(1)
})

这条用例与“已有近光是否通过”是两个维度。前者处理并发收口,后者处理业务结果。不要因为加入灯态判断,就删掉原有的活动状态、当前命令编号或已完成标记。

五、抽取一个只负责超时近光判定的最小函数

这次不需要重新设计整套灯光状态机。普通模式已经给出需要保留的行为,只需把那条条件变成可复用的判定器,并为调用方显式传入是否允许已有近光状态满足当前命令。

可以先定义两个很小的结果类型:

enum TimeoutVerdictCode {
  SATISFIED_BY_LOW_STATE = 'SATISFIED_BY_LOW_STATE',
  NOT_SATISFIED = 'NOT_SATISFIED'
}

interface TimeoutVerdict {
  passed: boolean
  code: TimeoutVerdictCode
}

function evaluateLowStateAtTimeout(
  expectedAction: string,
  currentLightState: string,
  acceptExistingLow: boolean
): TimeoutVerdict {
  const lowStateSatisfied =
    acceptExistingLow &&
    expectedAction === ACTION_LOW &&
    currentLightState === LIGHT_LOW

  if (lowStateSatisfied) {
    return {
      passed: true,
      code: TimeoutVerdictCode.SATISFIED_BY_LOW_STATE
    }
  }

  return {
    passed: false,
    code: TimeoutVerdictCode.NOT_SATISFIED
  }
}

这个函数刻意保持狭窄:

  1. 它只处理超时点的近光满足条件。
  2. 它不启动或取消计时器。
  3. 它不修改页面状态。
  4. 它不加分,也不写历史记录。
  5. 它不决定所有 ACTION_LOW 指令是否都允许沿用已有灯态。
  6. 它通过 acceptExistingLow 把命令策略留给调用方。

为什么不直接写成“只要 ACTION_LOW + LIGHT_LOW 就通过”?因为这次可以明确讨论的是普通模式的既有行为和 p_meet 的文案。其他实操指令即使也使用 ACTION_LOW,其语义仍需逐条确认。把开关作为参数,可以将影响限制在已分析的入口。

为什么返回 TimeoutVerdict 而不是单个 boolean?因为业务结果写入页面时,通常还需要区分“通过一次动作完成”和“在超时点由已有状态满足”。一个稳定的原因码有助于测试和排查,又不会让判定器直接依赖提示文案。

判定关系可以写成一个很小的真值表:

acceptExistingLowexpectedActioncurrentLightState结果
trueACTION_LOWLIGHT_LOW通过
trueACTION_LOWLIGHT_LOW失败
trueACTION_LOWLIGHT_LOW失败
falseACTION_LOWLIGHT_LOW失败

第四行让调用方可以继续维持其他实操命令的原行为,避免一次局部修复扩大成未经确认的业务改写。

六、普通灯光模式只替换条件,不改变收口顺序

超时判定职责结构

普通灯光模式已经接受 ACTION_LOW + LIGHT_LOW。接入共享函数时,目标是重构判断位置,不能改变外部行为。

建议结构如下,这仍是尚未写入项目的接入示例:

private handleTimeout(expectedAction: string): void {
  if (!this.isExamActive) {
    return
  }

  const verdict = evaluateLowStateAtTimeout(
    expectedAction,
    this.currentLightState,
    true
  )

  if (verdict.passed) {
    this.completeCurrentCommand(true, verdict.code)
    return
  }

  this.completeCurrentCommand(
    false,
    TimeoutVerdictCode.NOT_SATISFIED
  )
}

这里的第三个参数固定为 true,因为它对应普通模式已有的近光超时行为。接入前后,以下行为都应该保持不变:

  • 目标是近光,超时点已经为近光:继续通过。
  • 目标是近光,超时点不是近光:继续失败。
  • 目标不是近光,即使当前恰好为近光:继续失败。
  • 考试已经结束:旧回调直接返回。
  • 当前命令已经被点击路径收口:旧回调不能重复提交。

实际项目中的收口函数名、提示文本和分数更新方式应沿用现有实现。上面的 completeCurrentCommand 只是表示“原有普通模式收口逻辑”,不是对当前函数名的逐字摘录,也不能为了套用示例而新增一套并行状态。

普通模式接入共享函数还有一个作用:它让回归测试能直接证明抽取前后输出一致。如果只在实操函数里临时补一个 if,虽然也可能解决 p_meet,但普通模式仍保留另一份条件表达式,后续修改其中一处时差异可能再次出现。

可以用差异用例验证抽取没有改变普通模式:

const ordinaryCases: Array<TimeoutObservation> = [
  {
    expectedAction: ACTION_LOW,
    currentLightState: LIGHT_LOW,
    commandId: 'ordinary_low',
    passed: true
  },
  {
    expectedAction: ACTION_LOW,
    currentLightState: LIGHT_HIGH,
    commandId: 'ordinary_low',
    passed: false
  },
  {
    expectedAction: ACTION_HIGH,
    currentLightState: LIGHT_LOW,
    commandId: 'ordinary_high',
    passed: false
  }
]

ordinaryCases.forEach((item: TimeoutObservation) => {
  const verdict = evaluateLowStateAtTimeout(
    item.expectedAction,
    item.currentLightState,
    true
  )

  expect(verdict.passed).assertEqual(item.passed)
})

这组用例覆盖了条件中的三个变量方向:动作相同但状态不同、状态相同但动作不同,以及两者同时满足。只测成功用例会漏掉“其他动作被近光状态误放行”的风险。

七、实操模式只为 p_meet 打开已有近光分支

实操入口需要更谨慎。现有静态证据不足以支持把所有实操 ACTION_LOW 指令都改成状态型指令,因此先用命令编号明确限定 p_meet

可以增加一个简单策略函数:

function acceptsExistingLowAtTimeout(
  command: PracticalCommand
): boolean {
  return command.id === 'p_meet' &&
    command.action === ACTION_LOW
}

同时比较 idaction 有两个好处。

一是表达业务对象确实是 p_meet,而不是任何碰巧使用同一动作常量的命令。二是当题库以后误改了 p_meet 的动作字段时,函数不会悄悄继续放行,而会让回归用例暴露数据与策略不一致。

实操超时函数可以在原有保护条件之后调用共享判定器:

private handlePracticalTimeout(
  command: PracticalCommand
): void {
  if (!this.isPracticalExamActive) {
    return
  }

  if (this.currentPracticalCommand?.id !== command.id) {
    return
  }

  if (this.currentPracticalResolved) {
    return
  }

  const verdict = evaluateLowStateAtTimeout(
    command.action,
    this.currentLightState,
    acceptsExistingLowAtTimeout(command)
  )

  this.completeCurrentPracticalCommand(
    verdict.passed,
    verdict.code
  )
}

落地时仍要适配当前 Index.ets 的真实函数与收口入口。按这个边界修改后,p_meet 在超时点已经是近光时会通过;当前不是近光时仍失败。其他实操指令继续按原有超时失败路径收口。

这里还有一个容易混淆的选择:是否在 p_meet 展示出来的瞬间立刻判正确?

这次改动不应顺手加入“展示即完成”。普通模式的现有行为是在超时回调中检查已有近光状态,本次目标是让两条超时分支对齐。若实操模式一展示命令就自动跳到下一题,会改变学员阅读时间、语音播放、倒计时和页面反馈,影响范围明显更大。

因此,建议行为是:

  • 学员主动点击近光:继续由 handlePracticalAction 提前完成。
  • 学员没有点击,但近光始终保持到超时点:p_meet 由共享判定器判正确。
  • 学员在等待期间把近光切成其他灯态:超时点不满足,判失败。
  • 其他命令没有明确放开已有状态:继续按原路径处理。

这是一处局部对齐,不等于重新定义整个实操考试的交互节奏。

从命令读取到单次提交的端到端接线

把纯判断放回页面时,可以把一次超时处理拆成“读取命令、过滤旧回调、读取灯态、计算结果、单次提交”五段。这样排查时能定位到底是命令过期、灯态错误,还是结果被重复写入,而不用从最后一个布尔值向前猜。

阶段输入必须守住的条件输出
读取命令当前实操命令与回调命令编号两个编号必须一致当前有效命令
过滤会话活动标记与已收口标记会话活动且尚未收口是否继续
读取灯态页面统一灯态在有效回调中读取最新值currentLightState
计算结果动作、灯态、命令策略只有 p_meet 允许已有近光TimeoutVerdict
单次提交判定结果与原因码提交前再次检查收口标记一次提示、计分和切题

下面的 ArkTS 示例把这五段接在同一个入口中。它继续复用前文的 evaluateLowStateAtTimeout,没有另造第二套判断规则;函数名和状态字段仍需按真实页面适配。

private handlePracticalTimeout(commandId: string): void {
  if (!this.isPracticalExamActive ||
      this.currentPracticalResolved) {
    return
  }

  const command: PracticalCommand | undefined =
    this.currentPracticalCommand
  if (command === undefined || command.id !== commandId) {
    return
  }

  const currentLightState: string = this.currentLightState
  const acceptExistingLow: boolean =
    command.id === 'p_meet' && command.action === ACTION_LOW
  const verdict: TimeoutVerdict = evaluateLowStateAtTimeout(
    command.action,
    currentLightState,
    acceptExistingLow
  )

  if (this.currentPracticalResolved) {
    return
  }
  this.completeCurrentPracticalCommand(
    verdict.passed,
    verdict.code
  )
}

这段接线把旧回调过滤放在灯态读取和结果提交之前,并在提交前再次检查单次收口标记。它没有声称当前工程已经存在 currentPracticalResolved;若真实页面使用会话序号或索引变化保证幂等,应沿用原机制并用并发回归验证,而不必重复新增字段。

八、用差异回归证明只改变一个业务格子

修复后的回归不应只写一句“p_meet 通过”。更有价值的做法是列出修改前后矩阵,明确哪些结果必须变化,哪些结果必须保持。

场景修改前建议结果是否允许变化
普通模式,ACTION_LOW + LIGHT_LOW,超时通过通过不允许变化
普通模式,ACTION_LOW + LIGHT_HIGH,超时失败失败不允许变化
普通模式,其他动作 + LIGHT_LOW,超时失败失败不允许变化
实操 p_meet + LIGHT_LOW,超时失败通过本次唯一目标变化
实操 p_meet + LIGHT_HIGH,超时失败失败不允许变化
其他实操命令使用 ACTION_LOW,超时失败失败暂不改变
旧命令的延迟回调到达忽略忽略不允许变化
点击已完成后超时回调到达不应重复收口不应重复收口不允许变化

这张表可以直接转换为参数化测试:

interface PracticalTimeoutCase {
  name: string
  command: PracticalCommand
  lightState: string
  expectedPassed: boolean
}

const practicalCases: PracticalTimeoutCase[] = [
  {
    name: 'p_meet 已经是近光',
    command: {
      id: 'p_meet',
      text: '夜间与机动车会车:保证近光灯状态',
      action: ACTION_LOW,
      category: '会车'
    },
    lightState: LIGHT_LOW,
    expectedPassed: true
  },
  {
    name: 'p_meet 当前不是近光',
    command: {
      id: 'p_meet',
      text: '夜间与机动车会车:保证近光灯状态',
      action: ACTION_LOW,
      category: '会车'
    },
    lightState: LIGHT_HIGH,
    expectedPassed: false
  },
  {
    name: '其他近光动作没有获得状态豁免',
    command: {
      id: 'p_other_low',
      text: '执行近光灯操作',
      action: ACTION_LOW,
      category: '其他场景'
    },
    lightState: LIGHT_LOW,
    expectedPassed: false
  }
]

practicalCases.forEach((item: PracticalTimeoutCase) => {
  const verdict = evaluateLowStateAtTimeout(
    item.command.action,
    item.lightState,
    acceptsExistingLowAtTimeout(item.command)
  )

  expect(verdict.passed).assertEqual(item.expectedPassed)
})

第三条尤其重要。它证明本次修改依据的是 p_meet 的具体语义,而不是看到 ACTION_LOW 就批量改变所有实操命令。

还需要验证旧回调隔离。假设 p_meet 的定时器触发时,页面已经切换到下一条指令,回调中的灯态可能刚好是近光。如果先调用判定器、后比较命令编号,就可能把上一题结果写到下一题。因此顺序必须是:

  1. 确认实操会话仍处于活动状态。
  2. 确认回调携带的命令编号仍是当前编号。
  3. 确认当前命令尚未收口。
  4. 读取当前灯态并调用判定器。
  5. 执行一次终态提交。

可以给终态提交再加一道单次保护:

private completeCurrentPracticalCommand(
  passed: boolean,
  code: TimeoutVerdictCode
): void {
  if (this.currentPracticalResolved) {
    return
  }

  this.currentPracticalResolved = true
  this.clearPracticalTimer()

  // 以下继续调用项目现有的提示、计分和切题逻辑。
  this.applyPracticalResult(passed, code)
}

如果当前项目已经通过活动标记、索引变化或计时器清理保证单次收口,应优先复用现有机制,不必为了示例重复增加字段。关键是用测试确认“动作回调与超时回调相邻到达时只提交一次”。

九、排查时不要只盯着最后一个布尔值

即使判定器只有三项输入,接入页面后仍可能遇到“逻辑看起来正确,结果却没有变化”的情况。下面列出这类修改中较常见的原因。

现象优先检查常见原因处理方向
p_meet 已经是近光仍失败传入判定器的 currentLightState读取的是旧字段或局部副本在超时回调内读取当前统一灯态
p_meet 任何状态都通过判定条件是否同时比较动作与灯态使用了 `action === ACTION_LOW
其他近光实操题也通过acceptExistingLow 的来源直接对所有 ACTION_LOW 传入 true先限定 command.id === 'p_meet'
一题增加两次结果动作与超时的收口保护计时器已进入消息队列,单纯清理不足在终态提交处增加幂等保护
普通模式原来通过的场景变失败普通入口的第三个参数抽取后误传 false用普通模式三组回归锁定
切到下一题后上一题回调改写结果命令编号检查顺序判定和写入发生在旧回调过滤之前先比较活动状态与命令编号
页面提示与 passed 相反收口函数的参数顺序新增原因码后调用参数错位使用具名结果对象减少位置错误
用例通过但真机操作节奏异常是否加入了展示即完成修复范围扩大到命令呈现阶段恢复为仅在超时点读取状态

调试日志也应围绕这三个输入和一次提交,而不是打印整个页面对象。建议记录命令编号、期望动作、当前灯态、策略开关、结果原因和会话序号:

interface TimeoutTrace {
  sessionId: string
  commandId: string
  expectedAction: string
  currentLightState: string
  acceptExistingLow: boolean
  verdictCode: TimeoutVerdictCode
}

function createTimeoutTrace(
  sessionId: string,
  command: PracticalCommand,
  lightState: string,
  verdict: TimeoutVerdict
): TimeoutTrace {
  return {
    sessionId,
    commandId: command.id,
    expectedAction: command.action,
    currentLightState: lightState,
    acceptExistingLow: acceptsExistingLowAtTimeout(command),
    verdictCode: verdict.code
  }
}

这类结构化记录可以帮助判断问题出在题库数据、灯态读取、策略选择还是最终写入。正式接入时应沿用项目已有日志设施,并避免写入用户可识别信息。

如果看到 verdictCode 已经是 SATISFIED_BY_LOW_STATE,页面却仍显示失败,说明纯判断已经完成,问题应继续追到收口调用和页面状态更新,而不应反复修改 evaluateLowStateAtTimeout

十、验证清单与交付边界

在真正修改源码后,可以按下面顺序验证。前半部分是纯判断,后半部分才涉及页面和设备。

纯判断与分支回归

  • ACTION_LOW + LIGHT_LOW + acceptExistingLow=true 返回通过。
  • ACTION_LOW + 非 LIGHT_LOW + acceptExistingLow=true 返回失败。
  • 非 ACTION_LOW + LIGHT_LOW + acceptExistingLow=true 返回失败。
  • ACTION_LOW + LIGHT_LOW + acceptExistingLow=false 返回失败。
  • 普通模式抽取前后的三组输出保持一致。
  • p_meet + LIGHT_LOW 从实操旧结果失败变为建议结果通过。
  • p_meet + 非 LIGHT_LOW 仍为失败。
  • 其他实操命令没有被连带放行。
  • 旧命令的延迟回调不会写入当前命令。
  • 动作完成与超时到达相邻发生时只提交一次结果。

页面联调

  • 先执行一个结束后保持 LIGHT_LOW 的前置操作。
  • 进入 p_meet 时不再次点击近光按钮。
  • 等待本题超时,确认本题只生成一次正确结果。
  • 进入 p_meet 后切换到远光,再等待超时,确认结果为失败。
  • 主动点击近光时仍可沿用原动作处理路径提前完成。
  • 普通灯光模式的近光超时行为没有变化。
  • 提示文本、得分、进度和历史记录使用同一个最终结果。
  • 快速切题后,上一题定时器不会影响下一题。

当前事实、建议方案与尚未完成事项

层级本文结论
当前源码事实p_meet 的文案为“夜间与机动车会车:保证近光灯状态”,动作字段为 ACTION_LOW,分类为 会车
当前源码事实普通 handleTimeoutACTION_LOW + LIGHT_LOW 保留了超时通过分支
当前源码事实实操 handlePracticalTimeout 在有效回调进入后没有对应状态分支,会按失败收口
建议方案抽取最小的 evaluateLowStateAtTimeout,普通模式保持现状,实操模式只为 p_meet 打开已有近光满足条件
建议方案保留会话活动、命令编号和单次收口保护,用差异回归证明只改变目标格子
尚未完成本文代码没有写入 Index.ets 或其他项目源码
尚未完成没有运行项目构建,没有生成新的 HAP
尚未完成没有在模拟器或真机上重放 p_meet 场景
尚未完成没有形成安装包运行、触控过程或历史记录回读证据

源码阅读能确认的是两条超时分支当前存在不一致,建议代码说明的是一种局部对齐方法。只有完成实现、回归、构建和设备操作后,才能确认最终交互是否符合产品约定。因此这里只将它列为待实施方案,不把静态分支分析冒充为已经落地或通过真机验证的结果。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐