Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2083e71fb | ||
|
|
5206a6fd7d | ||
|
|
d1996d28ee | ||
|
|
7fdfe04658 | ||
|
|
53143131be | ||
|
|
650f3a6ba1 |
+2
@@ -248,6 +248,8 @@ class GoAutoAccessibilityService : AccessibilityService(), UiDriver, PddCollecto
|
||||
checked = node.isChecked,
|
||||
enabled = node.isEnabled,
|
||||
visible = node.isVisibleToUser,
|
||||
hintText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) node.hintText?.toString() else null,
|
||||
showingHintText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) node.isShowingHintText else null,
|
||||
)
|
||||
for (index in 0 until node.childCount) {
|
||||
node.getChild(index)?.let { snapshot(it, "$path/$index", path) }
|
||||
|
||||
+3
@@ -27,6 +27,9 @@ data class SnapshotNode(
|
||||
val checked: Boolean,
|
||||
val enabled: Boolean,
|
||||
val visible: Boolean,
|
||||
// In-memory accessibility metadata; not part of result or diagnostic payloads.
|
||||
val hintText: String? = null,
|
||||
val showingHintText: Boolean? = null,
|
||||
) {
|
||||
val label: String get() = text?.trim().takeUnless { it.isNullOrEmpty() }
|
||||
?: contentDescription?.trim().orEmpty()
|
||||
|
||||
+44
-7
@@ -545,16 +545,12 @@ class PurchaseLiveAutomation(
|
||||
val modify = addressModifyTargets(panel)
|
||||
if (modify.size != 1) fail("PURCHASE_ADDRESS_EDIT_AMBIGUOUS", "没有找到唯一的地址修改按钮,未创建订单")
|
||||
click(modify.single(), "修改地址")
|
||||
val edit = waitFor("PURCHASE_ADDRESS_EDIT_TIMEOUT", "地址编辑页面打开超时,未创建订单") { snapshot ->
|
||||
snapshot.nodes.any { it.visible && it.label.replace(" ", "").contains("详细地址") }
|
||||
}
|
||||
val editors = shippingAddressEditors(edit)
|
||||
if (editors.size != 1) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "没有找到唯一的详细地址输入框,未创建订单")
|
||||
val current = editors.single().label.trim()
|
||||
val editor = waitForInitialAddressEditor()
|
||||
val current = editor.text.orEmpty().trim()
|
||||
val body = current.split(Regex("[-_]"), limit = 2).firstOrNull()?.trim().orEmpty()
|
||||
if (body.isBlank()) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "当前详细地址无效,未创建订单")
|
||||
val expected = body + suffix
|
||||
if (driver.inputFresh(editors.single(), expected) != FreshActionResult.SUCCESS) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "详细地址修改失败,未创建订单")
|
||||
if (driver.inputFresh(editor, expected) != FreshActionResult.SUCCESS) fail("PURCHASE_ADDRESS_UPDATE_FAILED", "详细地址修改失败,未创建订单")
|
||||
val stable = waitFor("PURCHASE_ADDRESS_INPUT_MISMATCH", "详细地址修改后回读不一致,未创建订单") { snapshot ->
|
||||
snapshot.nodes.count { it.visible && it.enabled && it.className?.endsWith("EditText") == true && it.label == expected } == 1
|
||||
}
|
||||
@@ -575,6 +571,47 @@ class PurchaseLiveAutomation(
|
||||
return ShippingAddressProof(expected, suffix)
|
||||
}
|
||||
|
||||
/** Initial readiness only; return the same sampled node for the original value and fresh input anchor. */
|
||||
private fun waitForInitialAddressEditor(): SnapshotNode {
|
||||
fun isHint(editor: SnapshotNode): Boolean {
|
||||
val hint = editor.hintText?.trim().orEmpty()
|
||||
return editor.showingHintText == true || (hint.isNotEmpty() && editor.text?.trim() == hint)
|
||||
}
|
||||
var titleSeen = false
|
||||
var structural = emptyList<SnapshotNode>()
|
||||
var readyCount = 0
|
||||
repeat(50) {
|
||||
val snapshot = driver.capture()
|
||||
pageProblem(snapshot)
|
||||
val labels = snapshot.nodes.filter { it.visible && it.label.replace(" ", "").contains("详细地址") }
|
||||
titleSeen = titleSeen || labels.isNotEmpty()
|
||||
// Keep the existing same-row/rightward region, but count empty and disabled inputs too.
|
||||
// Their presence is structural ambiguity, even when just one input is ready.
|
||||
structural = snapshot.nodes.filter { editor ->
|
||||
editor.visible && editor.className?.endsWith("EditText") == true &&
|
||||
labels.any { label ->
|
||||
editor.bounds.top <= label.bounds.bottom &&
|
||||
editor.bounds.bottom >= label.bounds.top &&
|
||||
editor.bounds.left >= label.bounds.left
|
||||
}
|
||||
}.distinctBy { it.path }
|
||||
readyCount = structural.count { it.enabled && !it.text.isNullOrBlank() && !isHint(it) }
|
||||
if (structural.size == 1 && readyCount == 1) return structural.single()
|
||||
pause(200)
|
||||
}
|
||||
val reason = when {
|
||||
!titleSeen -> "title_missing"
|
||||
structural.isEmpty() -> "editor_missing"
|
||||
structural.size > 1 -> "editor_ambiguous"
|
||||
!structural.single().enabled -> "editor_disabled"
|
||||
structural.single().text.isNullOrBlank() -> "editor_empty"
|
||||
else -> "editor_hint"
|
||||
}
|
||||
val diagnostic = "stage=address_editor_ready;reason=$reason;titleSeen=$titleSeen;structural=${structural.size};ready=$readyCount"
|
||||
if (!titleSeen) fail("PURCHASE_ADDRESS_EDIT_TIMEOUT", "地址编辑页面打开超时,未创建订单 [$diagnostic]")
|
||||
fail("PURCHASE_ADDRESS_UPDATE_FAILED", "详细地址输入框未就绪,未创建订单 [$diagnostic]")
|
||||
}
|
||||
|
||||
private fun isPurchaseConfirmationPanel(snapshot: UiSnapshot): Boolean {
|
||||
if (snapshot.packageName != PDD_PACKAGE || shippingAddressEditors(snapshot).isNotEmpty()) return false
|
||||
val screen = PddScreenParser.parse(snapshot, PurchaseRehearsalExecutor.DEFAULT_COLLECTOR, "", null)
|
||||
|
||||
@@ -18,6 +18,77 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PurchaseLiveAutomationTest {
|
||||
@Test
|
||||
fun `hint delayed address update preserves fresh payment back budget for late order evidence`() {
|
||||
val observationDriver = OrderObservationDriver { sample ->
|
||||
observationPage(when {
|
||||
sample == 1 -> "payment"
|
||||
sample < 140 -> "loading"
|
||||
else -> "complete"
|
||||
})
|
||||
}.apply { backDuration = 4_000 }
|
||||
lateinit var addressDriver: LiveDriver
|
||||
addressDriver = LiveDriver(initialEditFrame = { snapshot, sample ->
|
||||
assertEquals(0, addressDriver.inputCount)
|
||||
assertFalse(addressDriver.clicked.contains("保存"))
|
||||
if (sample <= 4) observationDriver.elapsed += 10_000
|
||||
snapshot.changeAddressEditor {
|
||||
it.copy(
|
||||
text = if (sample <= 4) "合成提示占位标记" else "合成测试区域示例路段1号-old",
|
||||
hintText = "合成提示占位标记",
|
||||
showingHintText = sample <= 4,
|
||||
)
|
||||
}
|
||||
})
|
||||
var observingOrder = false
|
||||
val driver = object : PurchaseUiDriver by addressDriver {
|
||||
override fun capture(): UiSnapshot =
|
||||
if (observingOrder) observationDriver.capture() else addressDriver.capture()
|
||||
override fun backPurchase(): Boolean =
|
||||
if (observingOrder) observationDriver.backPurchase() else addressDriver.backPurchase()
|
||||
override fun clickFresh(target: SnapshotNode): FreshActionResult =
|
||||
if (observingOrder) observationDriver.clickFresh(target) else addressDriver.clickFresh(target)
|
||||
override fun swipePurchase(direction: SwipeDirection, durationMs: Long): Boolean =
|
||||
if (observingOrder) observationDriver.swipePurchase(direction, durationMs)
|
||||
else addressDriver.swipePurchase(direction, durationMs)
|
||||
override fun bringPddToForeground(): Boolean =
|
||||
if (observingOrder) observationDriver.bringPddToForeground() else addressDriver.bringPddToForeground()
|
||||
}
|
||||
val automation = PurchaseLiveAutomation(
|
||||
driver,
|
||||
pause = { observationDriver.elapsed += it },
|
||||
monotonicClockMs = { observationDriver.elapsed },
|
||||
)
|
||||
|
||||
val address = automation.updateShippingAddress("_cg366")
|
||||
assertEquals(5, addressDriver.initialEditCaptures)
|
||||
assertEquals("合成测试区域示例路段1号-old", addressDriver.lastInputTarget?.text)
|
||||
assertTrue(address.expectedAddress.endsWith("_cg366"))
|
||||
assertFalse(address.expectedAddress.contains("合成提示占位标记"))
|
||||
assertAddressEditorActions(addressDriver, saved = true)
|
||||
assertTrue(observationDriver.elapsed > 40_000)
|
||||
assertEquals("_cg366", automation.finalConfirmation(input().copy(addressSuffix = "_cg366"), address).addressSuffix)
|
||||
automation.submitOrderOnce()
|
||||
val clicksBeforeObservation = addressDriver.clicked.toList()
|
||||
observingOrder = true
|
||||
|
||||
val order = automation.readOrderResult()
|
||||
|
||||
assertEquals("PDD-DEMO-365", order?.orderNo)
|
||||
assertEquals("2026-10-08T03:21:43Z", order?.submittedAt)
|
||||
assertEquals(140, observationDriver.captures)
|
||||
val elapsedAfterBack = observationDriver.elapsed - observationDriver.backCompletedAt!!
|
||||
assertTrue(elapsedAfterBack in 28_000L until 30_000L)
|
||||
assertTrue(elapsedAfterBack + observationDriver.backDuration > 30_000)
|
||||
assertObservationActions(observationDriver, backs = 1)
|
||||
assertEquals(1, addressDriver.inputCount)
|
||||
assertEquals(1, addressDriver.clicked.count { it == "保存" })
|
||||
assertEquals(1, addressDriver.submitClicks)
|
||||
assertEquals(clicksBeforeObservation, addressDriver.clicked)
|
||||
assertEquals(0, addressDriver.genericSwipes + addressDriver.scopedSwipes)
|
||||
assertFalse(addressDriver.clicked.any { it.contains("支付") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `payment back observes late complete order beyond original sample budget`() {
|
||||
val driver = OrderObservationDriver { sample ->
|
||||
@@ -496,6 +567,338 @@ class PurchaseLiveAutomationTest {
|
||||
assertFalse(driver.clicked.any { it.contains("支付") })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor waits for an input arriving after its title`() {
|
||||
assertDelayedAddressEditor { snapshot, sample ->
|
||||
if (sample <= 4) snapshot.copy(nodes = snapshot.nodes.filterNot { it.path == "editor-address" }) else snapshot
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor waits for a populated value`() {
|
||||
assertDelayedAddressEditor { snapshot, sample ->
|
||||
snapshot.changeAddressEditor { if (sample <= 4) it.copy(text = " ") else it }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor description cannot substitute for empty text`() {
|
||||
for (text in listOf(null, "", " ")) {
|
||||
assertAddressEditorTimeout("editor_empty", 1, 0) { snapshot, _ ->
|
||||
snapshot.changeAddressEditor { it.copy(text = text, contentDescription = "合成说明占位标记") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor waits until hint is replaced by real text`() {
|
||||
assertDelayedAddressEditor { snapshot, sample ->
|
||||
snapshot.changeAddressEditor {
|
||||
it.copy(
|
||||
text = if (sample <= 4) "合成提示占位标记" else it.text,
|
||||
hintText = "合成提示占位标记",
|
||||
showingHintText = sample <= 4,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor persistent hint text fails after the full budget`() {
|
||||
assertAddressEditorTimeout("editor_hint", 1, 0) { snapshot, _ ->
|
||||
snapshot.changeAddressEditor {
|
||||
it.copy(text = "合成提示占位标记", hintText = "合成提示占位标记", showingHintText = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor rejects text equal to trimmed hint despite false or absent showing flag`() {
|
||||
for (showing in listOf(false, null)) {
|
||||
assertAddressEditorTimeout("editor_hint", 1, 0) { snapshot, _ ->
|
||||
snapshot.changeAddressEditor {
|
||||
it.copy(text = " 合成提示占位标记 ", hintText = " 合成提示占位标记 ", showingHintText = showing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor showing hint flag blocks nonmatching or missing hint text`() {
|
||||
for (hint in listOf(null, "", "合成提示占位标记")) {
|
||||
assertAddressEditorTimeout("editor_hint", 1, 0) { snapshot, _ ->
|
||||
snapshot.changeAddressEditor { it.copy(hintText = hint, showingHintText = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor empty text remains empty even when hint and description are present`() {
|
||||
assertAddressEditorTimeout("editor_empty", 1, 0) { snapshot, _ ->
|
||||
snapshot.changeAddressEditor {
|
||||
it.copy(text = " ", contentDescription = "合成说明占位标记", hintText = "合成提示占位标记", showingHintText = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor missing hint metadata remains compatible and uses actual text`() {
|
||||
val driver = LiveDriver(initialEditFrame = { snapshot, _ ->
|
||||
snapshot.changeAddressEditor {
|
||||
assertEquals(null, it.hintText)
|
||||
assertEquals(null, it.showingHintText)
|
||||
it.copy(text = " 合成真实地址-old ", contentDescription = "合成说明占位标记")
|
||||
}
|
||||
})
|
||||
|
||||
val proof = PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg366")
|
||||
|
||||
assertEquals("合成真实地址_cg366", proof.expectedAddress)
|
||||
assertEquals(1, driver.initialEditCaptures)
|
||||
assertTrue(driver.lastInputTarget === driver.lastInitialEditSnapshot?.nodes?.single { it.path == "editor-address" })
|
||||
assertAddressEditorActions(driver, saved = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor accepts actual text with absent false or blank hint metadata`() {
|
||||
for (showing in listOf(null, false)) {
|
||||
for (hint in listOf(null, "", " ", "合成提示占位标记")) {
|
||||
val driver = LiveDriver(initialEditFrame = { snapshot, _ ->
|
||||
snapshot.changeAddressEditor { it.copy(hintText = hint, showingHintText = showing) }
|
||||
})
|
||||
|
||||
val proof = PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg366")
|
||||
|
||||
assertTrue(proof.expectedAddress.endsWith("_cg366"))
|
||||
assertEquals(1, driver.initialEditCaptures)
|
||||
assertAddressEditorActions(driver, saved = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor waits for an enabled input`() {
|
||||
assertDelayedAddressEditor { snapshot, sample ->
|
||||
snapshot.changeAddressEditor { if (sample <= 4) it.copy(enabled = false) else it }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor tolerates more than three ambiguous frames before unique readiness`() {
|
||||
assertDelayedAddressEditor { snapshot, sample ->
|
||||
if (sample <= 4) snapshot.withSecondAddressEditor() else snapshot
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor uses the value and node from the successful readiness frame`() {
|
||||
val driver = LiveDriver(initialEditFrame = { snapshot, sample ->
|
||||
snapshot.changeAddressEditor { it.copy(text = if (sample == 1) "" else "合成地址第${sample}帧-old") }
|
||||
})
|
||||
|
||||
val proof = PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg366")
|
||||
|
||||
assertEquals("合成地址第2帧_cg366", proof.expectedAddress)
|
||||
assertEquals(2, driver.initialEditCaptures)
|
||||
assertTrue(driver.lastInputTarget === driver.lastInitialEditSnapshot?.nodes?.single { it.path == "editor-address" })
|
||||
assertAddressEditorActions(driver, saved = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor structural ambiguity waits full budget even with only one ready input`() {
|
||||
for (second in listOf<(SnapshotNode) -> SnapshotNode>(
|
||||
{ it.copy(text = "") }, { it.copy(enabled = false) },
|
||||
)) {
|
||||
assertAddressEditorTimeout("editor_ambiguous", structural = 2, ready = 1) { snapshot, _ ->
|
||||
snapshot.withSecondAddressEditor(second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor final zero inputs reports scalar missing reason`() {
|
||||
assertAddressEditorTimeout("editor_missing", 0, 0) { snapshot, _ ->
|
||||
snapshot.copy(nodes = snapshot.nodes.filterNot { it.path == "editor-address" })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor final empty input reports scalar empty reason`() {
|
||||
assertAddressEditorTimeout("editor_empty", 1, 0) { snapshot, _ ->
|
||||
snapshot.changeAddressEditor { it.copy(text = " ") }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor final disabled input reports scalar disabled reason`() {
|
||||
assertAddressEditorTimeout("editor_disabled", 1, 0) { snapshot, _ ->
|
||||
snapshot.changeAddressEditor { it.copy(enabled = false) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor final multiple inputs reports scalar ambiguity reason`() {
|
||||
assertAddressEditorTimeout("editor_ambiguous", 2, 2) { snapshot, _ -> snapshot.withSecondAddressEditor() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor never seeing a title keeps edit timeout and one shared budget`() {
|
||||
assertAddressEditorTimeout("title_missing", 0, 0, titleSeen = false) { snapshot, _ ->
|
||||
snapshot.copy(nodes = snapshot.nodes.filterNot { it.path == "detail" })
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor remembers a title even when the final snapshot is empty`() {
|
||||
assertAddressEditorTimeout("editor_missing", 0, 0) { snapshot, sample ->
|
||||
if (sample == 1) snapshot.copy(nodes = snapshot.nodes.filterNot { it.path == "editor-address" })
|
||||
else snapshot.copy(nodes = emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor late title does not start a second wait budget`() {
|
||||
assertAddressEditorTimeout("editor_empty", 1, 0) { snapshot, sample ->
|
||||
val emptyEditor = snapshot.changeAddressEditor { it.copy(text = "") }
|
||||
if (sample < 50) emptyEditor.copy(nodes = emptyEditor.nodes.filterNot { it.path == "detail" }) else emptyEditor
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor can become ready on the final sample of its shared budget`() {
|
||||
val driver = LiveDriver(initialEditFrame = { snapshot, sample ->
|
||||
if (sample < 50) snapshot.copy(nodes = emptyList()) else snapshot
|
||||
})
|
||||
val editPauses = mutableListOf<Long>()
|
||||
|
||||
PurchaseLiveAutomation(driver, pause = {
|
||||
if (driver.currentPage == "edit" && driver.inputCount == 0) editPauses += it
|
||||
}).updateShippingAddress("_cg366")
|
||||
|
||||
assertEquals(50, driver.initialEditCaptures)
|
||||
assertEquals(listOf(500L) + List(49) { 200L }, editPauses)
|
||||
assertAddressEditorActions(driver, saved = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor excludes recipient phone invisible and leftward inputs and deduplicates paths`() {
|
||||
val driver = LiveDriver(initialEditFrame = { snapshot, _ ->
|
||||
val editor = snapshot.nodes.single { it.path == "editor-address" }
|
||||
snapshot.copy(nodes = snapshot.nodes + listOf(
|
||||
editor,
|
||||
editor.copy(path = "hidden-input", visible = false),
|
||||
editor.copy(path = "left-input", bounds = NodeBounds(0, 390, 10, 480)),
|
||||
))
|
||||
})
|
||||
|
||||
PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg366")
|
||||
|
||||
assertEquals(1, driver.initialEditCaptures)
|
||||
assertEquals("editor-address", driver.lastInputTargetPath)
|
||||
assertAddressEditorActions(driver, saved = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor small screen row overlap remains ambiguous`() {
|
||||
assertAddressEditorTimeout("editor_ambiguous", 2, 2) { snapshot, _ ->
|
||||
snapshot.copy(nodes = snapshot.nodes.map {
|
||||
if (it.path == "editor-phone") it.copy(bounds = NodeBounds(180, 360, 900, 410)) else it
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor fresh input failures never retry or save`() {
|
||||
for (result in FreshActionResult.values().filter { it != FreshActionResult.SUCCESS }) {
|
||||
val driver = LiveDriver(inputResult = result)
|
||||
|
||||
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg366") }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
|
||||
assertEquals("PURCHASE_ADDRESS_UPDATE_FAILED", error.code)
|
||||
assertEquals(1, driver.inputCount)
|
||||
assertEquals(1, driver.initialEditCaptures)
|
||||
assertEquals(listOf("138****5678", "修改"), driver.clicked)
|
||||
assertEquals(0, driver.backCount)
|
||||
assertEquals(0, driver.scopedSwipes + driver.genericSwipes + driver.submitClicks)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `address editor page safety problem interrupts readiness without input`() {
|
||||
val driver = LiveDriver(initialEditFrame = { snapshot, sample ->
|
||||
if (sample == 1) snapshot.changeAddressEditor { it.copy(text = "") }
|
||||
else snapshot.copy(nodes = snapshot.nodes.map { if (it.path == "title") it.copy(text = "立即支付") else it })
|
||||
})
|
||||
|
||||
val error = runCatching { PurchaseLiveAutomation(driver, pause = {}).updateShippingAddress("_cg366") }
|
||||
.exceptionOrNull() as PurchaseLiveException
|
||||
|
||||
assertEquals("PURCHASE_PAYMENT_FORBIDDEN", error.code)
|
||||
assertEquals(2, driver.initialEditCaptures)
|
||||
assertAddressEditorActions(driver, saved = false)
|
||||
}
|
||||
|
||||
private fun assertDelayedAddressEditor(frame: (UiSnapshot, Int) -> UiSnapshot) {
|
||||
val driver = LiveDriver(initialEditFrame = frame)
|
||||
val editPauses = mutableListOf<Long>()
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {
|
||||
if (driver.currentPage == "edit" && driver.inputCount == 0) editPauses += it
|
||||
})
|
||||
|
||||
val proof = automation.updateShippingAddress("_cg366")
|
||||
|
||||
assertTrue(proof.expectedAddress.endsWith("_cg366"))
|
||||
assertEquals(5, driver.initialEditCaptures)
|
||||
assertEquals(listOf(500L, 200L, 200L, 200L, 200L), editPauses)
|
||||
assertEquals("editor-address", driver.lastInputTargetPath)
|
||||
assertAddressEditorActions(driver, saved = true)
|
||||
}
|
||||
|
||||
private fun assertAddressEditorTimeout(
|
||||
reason: String,
|
||||
structural: Int,
|
||||
ready: Int,
|
||||
titleSeen: Boolean = true,
|
||||
frame: (UiSnapshot, Int) -> UiSnapshot,
|
||||
) {
|
||||
val driver = LiveDriver(initialEditFrame = frame)
|
||||
val diagnostics = mutableListOf<String>()
|
||||
val editPauses = mutableListOf<Long>()
|
||||
val automation = PurchaseLiveAutomation(driver, pause = {
|
||||
if (driver.currentPage == "edit" && driver.inputCount == 0) editPauses += it
|
||||
}, panelDiagnostic = diagnostics::add)
|
||||
|
||||
val failure = runCatching { automation.updateShippingAddress("_cg366") }.exceptionOrNull()
|
||||
assertTrue("Readiness must fail without changing the address", failure is PurchaseLiveException)
|
||||
val error = failure as PurchaseLiveException
|
||||
|
||||
assertEquals(if (titleSeen) "PURCHASE_ADDRESS_UPDATE_FAILED" else "PURCHASE_ADDRESS_EDIT_TIMEOUT", error.code)
|
||||
val diagnostic = "stage=address_editor_ready;reason=$reason;titleSeen=$titleSeen;structural=$structural;ready=$ready"
|
||||
assertTrue(error.message.orEmpty(), error.message.orEmpty().endsWith("[$diagnostic]"))
|
||||
assertEquals(50, driver.initialEditCaptures)
|
||||
assertEquals(listOf(500L) + List(50) { 200L }, editPauses)
|
||||
val emitted = error.message.orEmpty() + diagnostics.joinToString()
|
||||
listOf("广东", "骏景", "测试收货人", "13800000000", "editor-address", "_cg366", "合成提示占位标记", "合成说明占位标记").forEach {
|
||||
assertFalse("Sensitive fixture value in readiness diagnostics", emitted.contains(it))
|
||||
}
|
||||
assertAddressEditorActions(driver, saved = false)
|
||||
}
|
||||
|
||||
private fun assertAddressEditorActions(driver: LiveDriver, saved: Boolean) {
|
||||
assertEquals(if (saved) 1 else 0, driver.inputCount)
|
||||
assertEquals(listOf("138****5678", "修改") + if (saved) listOf("保存") else emptyList(), driver.clicked)
|
||||
assertEquals(if (saved) 1 else 0, driver.backCount)
|
||||
assertEquals(1, driver.addressPathClicks)
|
||||
assertEquals(0, driver.addressTaps + driver.genericSwipes + driver.scopedSwipes + driver.submitClicks)
|
||||
}
|
||||
|
||||
private fun UiSnapshot.changeAddressEditor(change: (SnapshotNode) -> SnapshotNode): UiSnapshot =
|
||||
copy(nodes = nodes.map { if (it.path == "editor-address") change(it) else it })
|
||||
|
||||
private fun UiSnapshot.withSecondAddressEditor(change: (SnapshotNode) -> SnapshotNode = { it }): UiSnapshot =
|
||||
copy(nodes = nodes + change(nodes.single { it.path == "editor-address" }.copy(path = "editor-address-2")))
|
||||
|
||||
@Test
|
||||
fun `split confirmation address keeps the unique saved task suffix proof`() {
|
||||
val driver = LiveDriver(splitConfirmationAddress = true)
|
||||
@@ -1065,6 +1468,8 @@ class PurchaseLiveAutomationTest {
|
||||
private val orderDetailEntryAfterSubmit: Boolean = false,
|
||||
private val orderDetailEvidenceBelowFold: Boolean = false,
|
||||
private val duplicateOrderDetailEntry: Boolean = false,
|
||||
private val initialEditFrame: (UiSnapshot, Int) -> UiSnapshot = { snapshot, _ -> snapshot },
|
||||
private val inputResult: FreshActionResult = FreshActionResult.SUCCESS,
|
||||
postSubmitCaptureSequence: List<String> = emptyList(),
|
||||
) : PurchaseUiDriver {
|
||||
private var page = "confirmation"
|
||||
@@ -1079,6 +1484,10 @@ class PurchaseLiveAutomationTest {
|
||||
var addressTaps = 0
|
||||
var addressPathClicks = 0
|
||||
var lastInputTargetPath: String? = null
|
||||
var lastInputTarget: SnapshotNode? = null
|
||||
var lastInitialEditSnapshot: UiSnapshot? = null
|
||||
var initialEditCaptures = 0
|
||||
var inputCount = 0
|
||||
var backCount = 0
|
||||
var postSubmitBackCount = 0
|
||||
var pddRestoreCount = 0
|
||||
@@ -1091,7 +1500,12 @@ class PurchaseLiveAutomationTest {
|
||||
postSubmitCaptureCount++
|
||||
if (pendingPostSubmitPages.isNotEmpty()) page = pendingPostSubmitPages.removeAt(0)
|
||||
}
|
||||
return currentSnapshot()
|
||||
val snapshot = currentSnapshot()
|
||||
if (page == "edit" && inputCount == 0) {
|
||||
initialEditCaptures++
|
||||
return initialEditFrame(snapshot, initialEditCaptures).also { lastInitialEditSnapshot = it }
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
private fun currentSnapshot(): UiSnapshot = when (page) {
|
||||
@@ -1263,7 +1677,10 @@ class PurchaseLiveAutomationTest {
|
||||
}
|
||||
|
||||
override fun inputFresh(target: SnapshotNode, value: String): FreshActionResult {
|
||||
inputCount++
|
||||
lastInputTarget = target
|
||||
lastInputTargetPath = target.path
|
||||
if (inputResult != FreshActionResult.SUCCESS) return inputResult
|
||||
address = value
|
||||
return FreshActionResult.SUCCESS
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user