Fix conversation folder search targeting

This commit is contained in:
kris
2026-04-05 14:51:01 +08:00
parent 272698234d
commit a46f11cf6c
8 changed files with 426 additions and 34 deletions

View File

@@ -45,11 +45,15 @@ public class ConversationFolderActivityTest {
ImageButton headerAction = activity.findViewById(R.id.screen_header_action);
ImageButton refreshButton = activity.findViewById(R.id.screen_refresh_button);
LinearLayout content = activity.findViewById(R.id.screen_content);
TextView titleView = activity.findViewById(R.id.screen_title);
TextView subtitleView = activity.findViewById(R.id.screen_subtitle);
assertEquals("更多", String.valueOf(headerAction.getContentDescription()));
assertEquals(View.GONE, refreshButton.getVisibility());
assertEquals("Talking", String.valueOf(titleView.getText()));
assertEquals("3 个线程", String.valueOf(subtitleView.getText()));
assertTrue(viewTreeContainsText(content, "Talking"));
assertTrue(viewTreeContainsText(content, "查询腾讯HAI GPU服务器"));
assertTrue(viewTreeContainsText(content, "项目内部线程页"));
ReflectionHelpers.callInstanceMethod(activity, "showMoreMenu");
@@ -59,27 +63,79 @@ public class ConversationFolderActivityTest {
assertTrue(viewTreeContainsText(listView.getAdapter().getView(0, null, listView), "刷新"));
}
@Test
public void conversationFolderHighlightsTargetThreadWhenSearchTargetIsProvided() throws Exception {
Intent intent = new Intent()
.putExtra(ConversationFolderActivity.EXTRA_FOLDER_KEY, "talking")
.putExtra(ConversationFolderActivity.EXTRA_FOLDER_NAME, "Talking")
.putExtra(ConversationFolderActivity.EXTRA_TARGET_PROJECT_ID, "project-1")
.putExtra(ConversationFolderActivity.EXTRA_TARGET_PROJECT_IDS, new String[]{"project-1", "project-2"})
.putExtra(ConversationFolderActivity.EXTRA_TARGET_PROJECT_LABEL, "发布回滚");
TestConversationFolderActivity activity = Robolectric
.buildActivity(TestConversationFolderActivity.class, intent)
.setup()
.get();
ReflectionHelpers.callInstanceMethod(
activity,
"renderFolder",
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildFolderPayload())
);
LinearLayout content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "已定位到目标线程"));
assertTrue(viewTreeContainsText(content, "目标线程"));
assertTrue(viewTreeContainsText(content, "发布回滚"));
assertEquals(2, countTextOccurrences(content, "目标线程"));
assertTrue(countTextOccurrences(content, "发布回滚") >= 3);
assertEquals(0, countTextOccurrences(content, "project-1"));
}
private static JSONObject buildFolderPayload() throws Exception {
JSONArray threads = new JSONArray()
.put(new JSONObject()
.put("projectId", "project-1")
.put("threadTitle", "查询腾讯HAI GPU服务器")
.put("threadTitle", "发布回滚")
.put("folderLabel", "Talking")
.put("lastMessagePreview", "已从设备导入线程")
.put("timeLabel", "02:28"))
.put(new JSONObject()
.put("projectId", "project-2")
.put("threadTitle", "状态栏显示CPU和内存")
.put("threadTitle", "发布回滚")
.put("folderLabel", "Talking")
.put("lastMessagePreview", "已从设备导入线程")
.put("timeLabel", "02:12"));
.put("timeLabel", "02:12"))
.put(new JSONObject()
.put("projectId", "project-3")
.put("threadTitle", "日志收口")
.put("folderLabel", "Talking")
.put("lastMessagePreview", "已从设备导入线程")
.put("timeLabel", "02:05"));
return new JSONObject()
.put("folderLabel", "Talking")
.put("deviceName", "Mac Studio")
.put("threadCount", 2)
.put("threadCount", 3)
.put("threads", threads);
}
private static int countTextOccurrences(View root, String expectedText) {
int count = 0;
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();
if (expectedText.contentEquals(text)) {
count += 1;
}
}
if (!(root instanceof ViewGroup)) {
return count;
}
ViewGroup group = (ViewGroup) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
count += countTextOccurrences(group.getChildAt(index), expectedText);
}
return count;
}
private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();

View File

@@ -59,7 +59,8 @@ public class MainActivityConversationSearchTest {
.put("projectTitle", "Boss")
.put("threadTitle", "Boss")
.put("folderLabel", "2 个线程 · 最近:发布回滚")
.put("searchAliases", new JSONArray().put("发布回滚").put("Android UI 收尾"))
.put("searchAliases", new JSONArray().put("发布回滚").put("发布回滚").put("Android UI 收尾"))
.put("searchTargetProjectIds", new JSONArray().put("thread-revert-1").put("thread-revert-2").put("thread-ui"))
.put("lastMessagePreview", "最近:发布回滚")
.put("latestReplyLabel", "11:00"));
@@ -68,6 +69,9 @@ public class MainActivityConversationSearchTest {
assertEquals(1, filtered.length());
assertEquals("folder-boss", filtered.optJSONObject(0).optString("projectId", ""));
assertEquals("发布回滚", filtered.optJSONObject(0).optString("searchMatchLabel", ""));
assertEquals("thread-revert-1", filtered.optJSONObject(0).optString("searchMatchProjectId", ""));
assertEquals(2, filtered.optJSONObject(0).optJSONArray("searchMatchProjectIds").length());
assertEquals("thread-revert-2", filtered.optJSONObject(0).optJSONArray("searchMatchProjectIds").optString(1, ""));
}
@Test
@@ -132,7 +136,8 @@ public class MainActivityConversationSearchTest {
.put("threadTitle", "Boss")
.put("lastMessagePreview", "最近:发布回滚")
.put("latestReplyLabel", "11:00")
.put("searchAliases", new JSONArray().put("发布回滚").put("Android UI 收尾"))));
.put("searchAliases", new JSONArray().put("发布回滚").put("发布回滚").put("Android UI 收尾"))
.put("searchTargetProjectIds", new JSONArray().put("thread-revert-1").put("thread-revert-2").put("thread-ui"))));
ReflectionHelpers.callInstanceMethod(activity, "showContent");
Shadows.shadowOf(activity.getMainLooper()).idle();
@@ -154,6 +159,10 @@ public class MainActivityConversationSearchTest {
assertEquals(ConversationFolderActivity.class.getName(), nextIntent.getComponent().getClassName());
assertEquals("mac-studio:boss", nextIntent.getStringExtra(ConversationFolderActivity.EXTRA_FOLDER_KEY));
assertEquals("Boss", nextIntent.getStringExtra(ConversationFolderActivity.EXTRA_FOLDER_NAME));
assertEquals("thread-revert-1", nextIntent.getStringExtra(ConversationFolderActivity.EXTRA_TARGET_PROJECT_ID));
assertEquals(2, nextIntent.getStringArrayExtra(ConversationFolderActivity.EXTRA_TARGET_PROJECT_IDS).length);
assertEquals("thread-revert-2", nextIntent.getStringArrayExtra(ConversationFolderActivity.EXTRA_TARGET_PROJECT_IDS)[1]);
assertEquals("发布回滚", nextIntent.getStringExtra(ConversationFolderActivity.EXTRA_TARGET_PROJECT_LABEL));
}
private static JSONArray buildConversations() throws Exception {

View File

@@ -239,6 +239,27 @@ public class MainActivityRealtimeTest {
assertEquals(0, apiClient.conversationsCalls);
}
@Test
public void refreshConversationsData_fallsBackToFlatConversationsFeedWhenHomeFeedFails() throws Exception {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().resume().get();
Shadows.shadowOf(activity.getMainLooper()).idle();
RecordingRejectedConversationSourceClient apiClient = new RecordingRejectedConversationSourceClient(
activity.getSharedPreferences("test-boss-api", Context.MODE_PRIVATE)
);
ReflectionHelpers.setField(activity, "apiClient", apiClient);
ReflectionHelpers.callInstanceMethod(activity, "showContent");
Shadows.shadowOf(activity.getMainLooper()).idle();
activity.refreshConversationsData();
waitFor(() -> apiClient.homeCalls > 0 && apiClient.conversationsCalls > 0);
assertEquals(1, apiClient.homeCalls);
assertEquals(1, apiClient.conversationsCalls);
JSONArray conversationsData = ReflectionHelpers.getField(activity, "conversationsData");
assertEquals("flat-thread", conversationsData.optJSONObject(0).optString("projectId", ""));
}
@Test
public void refreshAllData_prefersConversationHomeFeedOverFlatConversationsFeed() throws Exception {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().resume().get();
@@ -262,6 +283,31 @@ public class MainActivityRealtimeTest {
assertEquals(0, apiClient.conversationsCalls);
}
@Test
public void refreshAllData_fallsBackToFlatConversationsFeedWhenHomeFeedFails() throws Exception {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().resume().get();
Shadows.shadowOf(activity.getMainLooper()).idle();
RecordingRejectedConversationSourceClient apiClient = new RecordingRejectedConversationSourceClient(
activity.getSharedPreferences("test-boss-api", Context.MODE_PRIVATE)
);
ReflectionHelpers.setField(activity, "apiClient", apiClient);
ReflectionHelpers.callInstanceMethod(activity, "showContent");
Shadows.shadowOf(activity.getMainLooper()).idle();
ReflectionHelpers.callInstanceMethod(
activity,
"refreshAllData",
ReflectionHelpers.ClassParameter.from(JSONObject.class, new JSONObject())
);
waitFor(() -> apiClient.homeCalls > 0 && apiClient.conversationsCalls > 0);
assertEquals(1, apiClient.homeCalls);
assertEquals(1, apiClient.conversationsCalls);
JSONArray conversationsData = ReflectionHelpers.getField(activity, "conversationsData");
assertEquals("flat-thread", conversationsData.optJSONObject(0).optString("projectId", ""));
}
private static void waitFor(BooleanSupplier condition) throws Exception {
long deadlineAt = System.currentTimeMillis() + 2_000L;
while (System.currentTimeMillis() < deadlineAt) {
@@ -298,6 +344,81 @@ public class MainActivityRealtimeTest {
}
}
private static final class RecordingRejectedConversationSourceClient extends BossApiClient {
int homeCalls;
int conversationsCalls;
int sessionCalls;
int devicesCalls;
int settingsCalls;
int otaCalls;
RecordingRejectedConversationSourceClient(android.content.SharedPreferences prefs) {
super(prefs, "https://boss.hyzq.net");
}
@Override
public ApiResponse getConversationHome() throws java.io.IOException, org.json.JSONException {
homeCalls += 1;
return new ApiResponse(200, new JSONObject()
.put("ok", false)
.put("message", "HOME_UNAVAILABLE"));
}
@Override
public ApiResponse getConversations() throws java.io.IOException, org.json.JSONException {
conversationsCalls += 1;
return new ApiResponse(200, new JSONObject()
.put("ok", true)
.put("conversations", buildFlatConversations()));
}
@Override
public ApiResponse getSession() throws java.io.IOException, org.json.JSONException {
sessionCalls += 1;
return new ApiResponse(200, new JSONObject()
.put("ok", true)
.put("session", new JSONObject()
.put("account", "17600003315")
.put("displayName", "Boss 超级管理员")));
}
@Override
public ApiResponse getDevices() throws java.io.IOException, org.json.JSONException {
devicesCalls += 1;
return new ApiResponse(200, new JSONObject()
.put("ok", true)
.put("devices", new JSONArray()));
}
@Override
public ApiResponse getSettings() throws java.io.IOException, org.json.JSONException {
settingsCalls += 1;
return new ApiResponse(200, new JSONObject()
.put("ok", true)
.put("settings", new JSONObject().put("preferredEntryPoint", "conversations"))
.put("user", new JSONObject()));
}
@Override
public ApiResponse getOtaStatus() throws java.io.IOException, org.json.JSONException {
otaCalls += 1;
return new ApiResponse(200, new JSONObject()
.put("ok", true)
.put("hasOta", false));
}
private static JSONArray buildFlatConversations() throws org.json.JSONException {
return new JSONArray().put(new JSONObject()
.put("projectId", "flat-thread")
.put("conversationType", "single_device")
.put("projectTitle", "发布回滚")
.put("threadTitle", "发布回滚")
.put("folderLabel", "Boss")
.put("lastMessagePreview", "最近:发布回滚")
.put("latestReplyLabel", "11:00"));
}
}
private static final class RecordingConversationSourceClient extends BossApiClient {
int homeCalls;
int conversationsCalls;