feat: add conversation selection mode and swipe tabs

This commit is contained in:
kris
2026-04-04 00:32:57 +08:00
parent c30b0a66ae
commit 5157a0ac07
9 changed files with 468 additions and 25 deletions

View File

@@ -0,0 +1,116 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class MainActivityConversationSelectionTest {
@Test
public void conversationsSelectionMode_requiresAtLeastTwoSelectionsForGroupChat() throws Exception {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get();
ReflectionHelpers.setField(activity, "conversationsData", buildConversations());
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(activity, "enterConversationSelectionMode");
assertTrue(ReflectionHelpers.getField(activity, "conversationSelectionMode"));
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "发起群聊"));
assertTrue(viewTreeContainsText(content, "至少选择 2 个线程"));
ReflectionHelpers.callInstanceMethod(activity, "toggleConversationSelection",
ReflectionHelpers.ClassParameter.from(String.class, "thread-1"));
content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "已选 1 个线程"));
assertTrue(viewTreeContainsText(content, "至少选择 2 个线程"));
ReflectionHelpers.callInstanceMethod(activity, "toggleConversationSelection",
ReflectionHelpers.ClassParameter.from(String.class, "thread-2"));
content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "已选 2 个线程"));
assertFalse(viewTreeContainsText(content, "至少选择 2 个线程"));
}
@Test
public void selectionModeRowsRenderSelectorInTrailingArea() throws Exception {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get();
ReflectionHelpers.setField(activity, "conversationsData", buildConversations());
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(activity, "enterConversationSelectionMode");
LinearLayout content = activity.findViewById(R.id.screen_content);
View row = content.getChildAt(3);
assertTrue("多选模式应显示单选圆点", viewTreeContainsContentDescription(row, "未选中会话"));
}
private static JSONArray buildConversations() throws Exception {
return new JSONArray()
.put(new JSONObject()
.put("projectId", "thread-1")
.put("projectTitle", "Boss 移动控制台")
.put("threadTitle", "Boss 移动控制台")
.put("folderLabel", "Boss")
.put("lastMessagePreview", "线程链路正常")
.put("latestReplyLabel", "09:41")
.put("conversationType", "single_device"))
.put(new JSONObject()
.put("projectId", "thread-2")
.put("projectTitle", "硬件审计协作")
.put("threadTitle", "硬件审计协作")
.put("folderLabel", "Mac Studio")
.put("lastMessagePreview", "检查摄像头供电链路")
.put("latestReplyLabel", "09:42")
.put("conversationType", "single_device"));
}
private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();
if (expectedText.contentEquals(text)) {
return true;
}
}
if (!(root instanceof LinearLayout)) {
return false;
}
LinearLayout group = (LinearLayout) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
return true;
}
}
return false;
}
private static boolean viewTreeContainsContentDescription(View root, String expectedText) {
CharSequence description = root.getContentDescription();
if (expectedText.contentEquals(description)) {
return true;
}
if (!(root instanceof LinearLayout)) {
return false;
}
LinearLayout group = (LinearLayout) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
if (viewTreeContainsContentDescription(group.getChildAt(index), expectedText)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,32 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class MainActivitySwipeNavigationTest {
@Test
public void horizontalSwipeMovesBetweenRootTabs() {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get();
ReflectionHelpers.callInstanceMethod(activity, "showContent");
ReflectionHelpers.callInstanceMethod(activity, "handleHorizontalPageSwipe",
ReflectionHelpers.ClassParameter.from(int.class, 1));
assertEquals("devices", ReflectionHelpers.getField(activity, "activeTab"));
ReflectionHelpers.callInstanceMethod(activity, "handleHorizontalPageSwipe",
ReflectionHelpers.ClassParameter.from(int.class, 1));
assertEquals("me", ReflectionHelpers.getField(activity, "activeTab"));
ReflectionHelpers.callInstanceMethod(activity, "handleHorizontalPageSwipe",
ReflectionHelpers.ClassParameter.from(int.class, -1));
assertEquals("devices", ReflectionHelpers.getField(activity, "activeTab"));
}
}

View File

@@ -62,9 +62,24 @@ public class WechatSurfaceMapperConversationStatusTest {
WechatSurfaceMapper.ConversationRow row = WechatSurfaceMapper.toConversationRow(item);
assertNull(row.contextStatusLabel);
assertNull(row.contextStatusLevel);
assertEquals(-1, row.contextUsagePercent);
assertEquals("上下文稳定", row.contextStatusLabel);
assertEquals("safe", row.contextStatusLevel);
assertEquals(0, row.contextUsagePercent);
assertEquals(true, row.contextIndicatorVisible);
}
@Test
public void toConversationRow_usesSafeContextDefaultsWhenIndicatorMissing() {
JSONObject item = new StubJSONObject()
.withString("threadTitle", "北区试产线回归")
.withInt("activityIconCount", 0);
WechatSurfaceMapper.ConversationRow row = WechatSurfaceMapper.toConversationRow(item);
assertEquals("上下文稳定", row.contextStatusLabel);
assertEquals("safe", row.contextStatusLevel);
assertEquals(0, row.contextUsagePercent);
assertEquals(true, row.contextIndicatorVisible);
}
private static final class StubJSONObject extends JSONObject {

View File

@@ -8,13 +8,23 @@ import org.junit.Test;
public class WechatSurfaceMapperTopActionTest {
@Test
public void rootTopAction_usesPlusForConversations() {
WechatSurfaceMapper.RootTopAction action = WechatSurfaceMapper.rootTopAction("conversations", false);
public void rootTopAction_usesSelectionForConversations() {
WechatSurfaceMapper.RootTopAction action = WechatSurfaceMapper.rootTopAction("conversations", false, false);
assertEquals("+", action.label);
assertEquals("选择", action.label);
assertFalse(action.primaryStyle);
assertTrue(action.compactStyle);
assertEquals("create_group_chat", action.actionKey);
assertFalse(action.compactStyle);
assertEquals("select_conversations", action.actionKey);
}
@Test
public void rootTopAction_usesCancelDuringConversationSelection() {
WechatSurfaceMapper.RootTopAction action = WechatSurfaceMapper.rootTopAction("conversations", false, true);
assertEquals("取消", action.label);
assertFalse(action.primaryStyle);
assertFalse(action.compactStyle);
assertEquals("cancel_select_conversations", action.actionKey);
}
@Test