fix(android): retain verified spec selections (#224)
This commit is contained in:
+24
-12
@@ -608,7 +608,11 @@ class PurchaseRehearsalExecutor(
|
||||
return matchingCandidates.size == 1 && matchingCandidates.single() == target
|
||||
}
|
||||
|
||||
private data class ExactSpecSelectionProof(val dimension: String, val target: String)
|
||||
private data class ExactSpecSelectionProof(
|
||||
val dimension: String,
|
||||
val target: String,
|
||||
val panelType: SpecPanelType,
|
||||
)
|
||||
|
||||
private fun exactSpecSelectionProof(
|
||||
screen: ParsedPddScreen,
|
||||
@@ -620,7 +624,7 @@ class PurchaseRehearsalExecutor(
|
||||
val tokenCandidates = screen.dimensions.filter { it.key == dimension }.flatMap { it.values }.filter { candidate ->
|
||||
if (dimension == "size") SpecValueNormalizer.primarySizeToken(candidate.text) == token else candidate.text == token
|
||||
}
|
||||
return ExactSpecSelectionProof(dimension, target).takeIf {
|
||||
return ExactSpecSelectionProof(dimension, target, screen.specPanelType).takeIf {
|
||||
tokenCandidates.size == 1 && tokenCandidates.single().text == target
|
||||
}
|
||||
}
|
||||
@@ -631,6 +635,7 @@ class PurchaseRehearsalExecutor(
|
||||
val summaryTokenMatched: Boolean,
|
||||
val candidateCount: Int,
|
||||
val proofPresent: Boolean,
|
||||
val targetMatchCount: Int = 0,
|
||||
val relocationAttempted: Boolean = false,
|
||||
val relocationSwipes: Int = 0,
|
||||
)
|
||||
@@ -643,15 +648,21 @@ class PurchaseRehearsalExecutor(
|
||||
): FinalSpecVerification {
|
||||
val candidates = screen.dimensions.filter { it.key == dimension }.flatMap { it.values }
|
||||
val matchingNodes = candidates.filter { it.text == target }
|
||||
val proofPresent = proof?.dimension == dimension && proof.target == target
|
||||
val proofPresent = proof?.dimension == dimension && proof.target == target && proof.panelType == screen.specPanelType
|
||||
if (matchingNodes.size == 1 && (matchingNodes.single().node.selected || matchingNodes.single().node.checked)) {
|
||||
return FinalSpecVerification(true, "selected_node", false, candidates.size, proofPresent)
|
||||
return FinalSpecVerification(true, "selected_node", false, candidates.size, proofPresent, matchingNodes.size)
|
||||
}
|
||||
if (candidates.any { it.text != target && (it.node.selected || it.node.checked) }) {
|
||||
return FinalSpecVerification(false, "visible_selected_conflict", false, candidates.size, proofPresent, matchingNodes.size)
|
||||
}
|
||||
|
||||
val token = if (dimension == "size") SpecValueNormalizer.primarySizeToken(target) else target.trim()
|
||||
val summaryTokenMatched = !token.isNullOrBlank() && SpecValueNormalizer.summaryHasExactToken(screen.selectedSummary, token)
|
||||
if (!summaryTokenMatched) {
|
||||
return FinalSpecVerification(false, "summary_token_missing", false, candidates.size, proofPresent)
|
||||
if (screen.selectedSummary == null && screen.specPanelOpen && proofPresent) {
|
||||
return FinalSpecVerification(true, "attempt_selection_state", false, candidates.size, true, matchingNodes.size)
|
||||
}
|
||||
return FinalSpecVerification(false, "summary_token_missing", false, candidates.size, proofPresent, matchingNodes.size)
|
||||
}
|
||||
// PDD can rerender or scroll the previously selected dimension out of
|
||||
// the current viewport while the selected summary remains visible. A
|
||||
@@ -662,15 +673,15 @@ class PurchaseRehearsalExecutor(
|
||||
if (dimension == "size") SpecValueNormalizer.primarySizeToken(candidate.text) == token else candidate.text == token
|
||||
}
|
||||
if (visibleTokenMatches.size == 1 && visibleTokenMatches.single().text == target) {
|
||||
return FinalSpecVerification(true, "visible_candidate", true, candidates.size, proofPresent)
|
||||
return FinalSpecVerification(true, "visible_candidate", true, candidates.size, proofPresent, matchingNodes.size)
|
||||
}
|
||||
if (visibleTokenMatches.isNotEmpty()) {
|
||||
return FinalSpecVerification(false, "visible_candidate_conflict", true, candidates.size, proofPresent)
|
||||
return FinalSpecVerification(false, "visible_candidate_conflict", true, candidates.size, proofPresent, matchingNodes.size)
|
||||
}
|
||||
return if (proofPresent) {
|
||||
FinalSpecVerification(true, "attempt_selection_proof", true, candidates.size, true)
|
||||
FinalSpecVerification(true, "attempt_selection_proof", true, candidates.size, true, matchingNodes.size)
|
||||
} else {
|
||||
FinalSpecVerification(false, "selection_proof_missing", true, candidates.size, false)
|
||||
FinalSpecVerification(false, "selection_proof_missing", true, candidates.size, false, matchingNodes.size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,7 +696,7 @@ class PurchaseRehearsalExecutor(
|
||||
return FinalSpecVerification(false, it.code, false, 0, proof != null)
|
||||
}
|
||||
var verification = verifyExactSpecSelection(screen, dimension, target, proof)
|
||||
if (verification.confirmed || verification.candidateCount > 0) return verification
|
||||
if (verification.confirmed || verification.targetMatchCount > 0 || verification.reason == "visible_selected_conflict") return verification
|
||||
if (!screen.specPanelOpen) return verification.copy(reason = "relocation_panel_closed", relocationAttempted = true)
|
||||
|
||||
var signature = screen.dimensions.joinToString("|") { item ->
|
||||
@@ -728,7 +739,7 @@ class PurchaseRehearsalExecutor(
|
||||
relocationAttempted = true,
|
||||
relocationSwipes = swipes,
|
||||
)
|
||||
if (verification.confirmed || verification.candidateCount > 0) return verification
|
||||
if (verification.confirmed || verification.targetMatchCount > 0 || verification.reason == "visible_selected_conflict") return verification
|
||||
val refreshedSignature = screen.dimensions.joinToString("|") { item ->
|
||||
"${item.key}:${item.values.joinToString(",") { value -> "${value.text}:${value.available}" }}"
|
||||
}
|
||||
@@ -837,7 +848,8 @@ class PurchaseRehearsalExecutor(
|
||||
val panel = screen.specPanelType.name.lowercase()
|
||||
val diagnostic = "dimension=$dimension,reason=${verification.reason},panel=$panel," +
|
||||
"summary=${screen.selectedSummary != null},tokenMatched=${verification.summaryTokenMatched}," +
|
||||
"candidates=${verification.candidateCount},proof=${verification.proofPresent}," +
|
||||
"candidates=${verification.candidateCount},targetMatches=${verification.targetMatchCount}," +
|
||||
"proof=${verification.proofPresent}," +
|
||||
"relocated=${verification.relocationAttempted},relocationSwipes=${verification.relocationSwipes}"
|
||||
return failure(SPEC_SELECTION_UNCONFIRMED, "最终规格复核未能确认精确选中状态 [$diagnostic]")
|
||||
}
|
||||
|
||||
+13
-16
@@ -233,7 +233,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `final verification rejects prior proof when selected summary is no longer visible`() {
|
||||
fun `final verification keeps exact attempt selection when summary is no longer visible`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
hideColorAfterQuantitySet = true,
|
||||
hideSelectedSummaryAfterQuantitySet = true,
|
||||
@@ -241,15 +241,12 @@ class PurchaseRehearsalExecutorTest {
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = {})
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("failed", outcome.resultType)
|
||||
assertEquals("PURCHASE_SPEC_SELECTION_UNCONFIRMED", outcome.errorCode)
|
||||
assertTrue(outcome.message.contains("dimension=color"))
|
||||
assertTrue(outcome.message.contains("reason=summary_token_missing"))
|
||||
assertTrue(outcome.message.contains("proof=true"))
|
||||
assertEquals("rehearsal_completed", outcome.resultType)
|
||||
assertEquals(1, driver.clicked.count { it == "黑色" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `final verification relocates hidden selected color when summary is unavailable`() {
|
||||
fun `final verification reuses exact attempt state without scrolling back`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
hideColorAfterQuantitySet = true,
|
||||
hideSelectedSummaryAfterQuantitySet = true,
|
||||
@@ -259,18 +256,17 @@ class PurchaseRehearsalExecutorTest {
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
|
||||
assertEquals("rehearsal_completed", outcome.resultType)
|
||||
assertTrue(driver.swipeInPaths.contains("scroll"))
|
||||
assertFalse(driver.swipeInPaths.contains("scroll"))
|
||||
assertEquals(1, driver.clicked.count { it == "黑色" })
|
||||
assertEquals(1, driver.clicked.count { it == "XL" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `final verification rejects relocated color without fresh selected state`() {
|
||||
fun `final verification rejects explicit conflicting selected color`() {
|
||||
val driver = FakePurchaseDriver(
|
||||
hideColorAfterQuantitySet = true,
|
||||
colors = listOf("黑色", "白色"),
|
||||
hideSelectedSummaryAfterQuantitySet = true,
|
||||
restoreHiddenColorOnDownSwipe = true,
|
||||
hideRestoredColorSelectedState = true,
|
||||
selectedColorOverrideAfterQuantitySet = "白色",
|
||||
)
|
||||
val outcome = PurchaseRehearsalExecutor(driver, { driver.browser = true; true }, { null }, pause = {})
|
||||
.execute(input(), PurchaseRuleParser.parse(rule()), PurchaseAgentCapabilities.supported)
|
||||
@@ -278,8 +274,8 @@ class PurchaseRehearsalExecutorTest {
|
||||
assertEquals("failed", outcome.resultType)
|
||||
assertEquals("PURCHASE_SPEC_SELECTION_UNCONFIRMED", outcome.errorCode)
|
||||
assertTrue(outcome.message.contains("dimension=color"))
|
||||
assertTrue(outcome.message.contains("relocated=true"))
|
||||
assertTrue(outcome.message.contains("relocationSwipes=1"))
|
||||
assertTrue(outcome.message.contains("reason=visible_selected_conflict"))
|
||||
assertTrue(outcome.message.contains("targetMatches=1"))
|
||||
assertEquals(1, driver.clicked.count { it == "黑色" })
|
||||
}
|
||||
|
||||
@@ -986,7 +982,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
private val hideSizeAfterQuantitySet: Boolean = false,
|
||||
private val hideSelectedSummaryAfterQuantitySet: Boolean = false,
|
||||
private val restoreHiddenColorOnDownSwipe: Boolean = false,
|
||||
private val hideRestoredColorSelectedState: Boolean = false,
|
||||
private val selectedColorOverrideAfterQuantitySet: String? = null,
|
||||
private val selectedSummaryOverrideAfterQuantitySet: String? = null,
|
||||
private val finalSizesAfterQuantitySet: List<String>? = null,
|
||||
soldOut: Boolean = false,
|
||||
@@ -1102,6 +1098,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
}
|
||||
if (!hideColor) {
|
||||
nodes += node("scroll/color-heading", "颜色分类", 20, 410, 300, 450, parentPath = "scroll")
|
||||
val selectedColor = if (quantity == 2L) selectedColorOverrideAfterQuantitySet ?: color else color
|
||||
colors.forEachIndexed { index, value ->
|
||||
nodes += node(
|
||||
"scroll/color-$index",
|
||||
@@ -1111,7 +1108,7 @@ class PurchaseRehearsalExecutorTest {
|
||||
200 + index * 220,
|
||||
540,
|
||||
clickable = true,
|
||||
selected = color == value && !(quantity == 2L && hiddenColorRestored && hideRestoredColorSelectedState),
|
||||
selected = selectedColor == value,
|
||||
enabled = !allSpecsUnavailable,
|
||||
parentPath = "scroll",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user