feat: harden ai onboarding and approval chat flows
This commit is contained in:
@@ -306,27 +306,17 @@ public class AiAccountsActivity extends BossScreenActivity {
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.onboardOpenAiApiAccount(payload);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
|
||||
String accountId = extractAccountId(response.json);
|
||||
if (accountId.isEmpty()) {
|
||||
runOnUiThread(() -> {
|
||||
showMessage("OpenAI 平台账号已登录,并设为当前主控。");
|
||||
reload();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
BossApiClient.ApiResponse validation = apiClient.validateAccount(accountId);
|
||||
runOnUiThread(() -> {
|
||||
showMessage(validation.ok()
|
||||
? validation.message()
|
||||
: "登录完成,但校验失败:" + validation.message());
|
||||
showMessage("OpenAI 平台账号已登录,并设为当前主控。");
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("登录失败:" + error.getMessage());
|
||||
String detail = error.getMessage();
|
||||
showMessage(detail == null || detail.trim().isEmpty()
|
||||
? "OpenAI 平台账号登录失败,请稍后重试。"
|
||||
: "OpenAI 平台账号登录失败:" + detail);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,10 +111,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private static final class ProjectSnapshot {
|
||||
final JSONObject payload;
|
||||
final @Nullable JSONArray dispatchPlans;
|
||||
final @Nullable JSONObject participantsPayload;
|
||||
|
||||
ProjectSnapshot(JSONObject payload, @Nullable JSONArray dispatchPlans) {
|
||||
ProjectSnapshot(JSONObject payload, @Nullable JSONArray dispatchPlans, @Nullable JSONObject participantsPayload) {
|
||||
this.payload = payload;
|
||||
this.dispatchPlans = dispatchPlans;
|
||||
this.participantsPayload = participantsPayload;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,7 +230,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
ProjectSnapshot snapshot = fetchProjectSnapshot();
|
||||
runOnUiThread(() -> renderProject(snapshot.payload, snapshot.dispatchPlans));
|
||||
runOnUiThread(() -> renderProject(snapshot.payload, snapshot.dispatchPlans, snapshot.participantsPayload));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
@@ -257,7 +259,11 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
updateSelectionUi();
|
||||
}
|
||||
|
||||
private void renderProject(JSONObject payload, @Nullable JSONArray dispatchPlans) {
|
||||
private void renderProject(
|
||||
JSONObject payload,
|
||||
@Nullable JSONArray dispatchPlans,
|
||||
@Nullable JSONObject participantsPayload
|
||||
) {
|
||||
JSONObject project = payload.optJSONObject("project");
|
||||
JSONArray devices = payload.optJSONArray("devices");
|
||||
JSONObject threadMeta = project == null ? null : project.optJSONObject("threadMeta");
|
||||
@@ -278,6 +284,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (currentPendingDispatchPlan != null) {
|
||||
appendContent(buildPendingDispatchPlanView(currentPendingDispatchPlan));
|
||||
}
|
||||
if (projectIsGroup && participantsPayload != null && participantsPayload.optBoolean("repairRequired", false)) {
|
||||
appendContent(buildRepairGroupMembersView(participantsPayload));
|
||||
}
|
||||
|
||||
JSONArray messages = project == null ? null : project.optJSONArray("messages");
|
||||
selectionState = ProjectChatUiState.reconcileSelection(selectionState, collectMessageIds(messages));
|
||||
@@ -585,7 +594,29 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
));
|
||||
Button confirmButton = BossUi.buildMiniActionButton(this, "确认下发", true);
|
||||
confirmButton.setOnClickListener(v -> showDispatchPlanConfirmation(dispatchPlan));
|
||||
container.addView(BossUi.buildInlineActionRow(this, confirmButton));
|
||||
Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false);
|
||||
rejectButton.setOnClickListener(v -> rejectDispatchPlan(dispatchPlan));
|
||||
container.addView(BossUi.buildInlineActionRow(this, confirmButton, rejectButton));
|
||||
return container;
|
||||
}
|
||||
|
||||
private View buildRepairGroupMembersView(JSONObject participantsPayload) {
|
||||
String repairReason = participantsPayload.optString("repairReason", "当前群聊里有失效线程,请先修复群成员。");
|
||||
int invalidParticipantCount = participantsPayload.optInt("invalidParticipantCount", 0);
|
||||
String meta = invalidParticipantCount > 0
|
||||
? "存在 " + invalidParticipantCount + " 个失效成员"
|
||||
: "当前群聊还没有可下发的真实线程";
|
||||
LinearLayout container = new LinearLayout(this);
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
container.addView(BossUi.buildCard(
|
||||
this,
|
||||
"修复群成员",
|
||||
repairReason,
|
||||
meta
|
||||
));
|
||||
Button repairButton = BossUi.buildMiniActionButton(this, "去修复", true);
|
||||
repairButton.setOnClickListener(v -> openConversationInfo());
|
||||
container.addView(BossUi.buildInlineActionRow(this, repairButton));
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -1532,6 +1563,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
throw new IllegalStateException(detailResponse.message());
|
||||
}
|
||||
JSONArray dispatchPlans = null;
|
||||
JSONObject participantsPayload = null;
|
||||
if (includeDispatchPlans) {
|
||||
try {
|
||||
BossApiClient.ApiResponse dispatchPlansResponse = apiClient.getDispatchPlans(projectId);
|
||||
@@ -1541,8 +1573,16 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
} catch (Exception ignored) {
|
||||
dispatchPlans = null;
|
||||
}
|
||||
try {
|
||||
BossApiClient.ApiResponse participantsResponse = apiClient.getConversationParticipants(projectId);
|
||||
if (participantsResponse.ok()) {
|
||||
participantsPayload = participantsResponse.json;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
participantsPayload = null;
|
||||
}
|
||||
}
|
||||
return new ProjectSnapshot(detailResponse.json, dispatchPlans);
|
||||
return new ProjectSnapshot(detailResponse.json, dispatchPlans, participantsPayload);
|
||||
}
|
||||
|
||||
private void startReplyWait(
|
||||
@@ -1571,7 +1611,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
|
||||
if (!renderedInitialSnapshot || hasReply) {
|
||||
runOnUiThread(() -> {
|
||||
renderProject(snapshot.payload, snapshot.dispatchPlans);
|
||||
renderProject(snapshot.payload, snapshot.dispatchPlans, snapshot.participantsPayload);
|
||||
if (!hasReply) {
|
||||
composerSending = true;
|
||||
updateComposerSendButtonState();
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Looper;
|
||||
|
||||
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.shadows.ShadowToast;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class AiAccountsActivityTest {
|
||||
@Test
|
||||
public void submitOpenAiOnboarding_reportsExplicitPrimaryControllerSuccessAndRefreshesSummary() throws Exception {
|
||||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||||
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(
|
||||
new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/accounts/onboard/openai-api"),
|
||||
200,
|
||||
"{\"ok\":true,\"accountId\":\"acc-1\"}",
|
||||
"{\"ok\":false,\"message\":\"ONBOARD_FAILED\"}"
|
||||
),
|
||||
new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/accounts/acc-1/validate"),
|
||||
200,
|
||||
"{\"ok\":true,\"message\":\"校验通过\"}",
|
||||
"{\"ok\":false,\"message\":\"VALIDATION_FAILED\"}"
|
||||
)
|
||||
));
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
int initialReloadCount = activity.reloadCount;
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"submitOpenAiOnboarding",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "主 GPT"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "OpenAI 平台账号"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "sk-test"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "gpt-5.4"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "sk-test-key")
|
||||
);
|
||||
org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
assertEquals("OpenAI 平台账号已登录,并设为当前主控。", ShadowToast.getTextOfLatestToast());
|
||||
assertEquals(initialReloadCount + 1, activity.reloadCount);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void submitOpenAiOnboarding_showsClearChineseFailurePrefix() throws Exception {
|
||||
TestAiAccountsActivity activity = Robolectric.buildActivity(TestAiAccountsActivity.class).setup().get();
|
||||
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(
|
||||
new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/accounts/onboard/openai-api"),
|
||||
403,
|
||||
"{\"ok\":false,\"message\":\"API Key 无效\"}",
|
||||
"{\"ok\":false,\"message\":\"API Key 无效\"}"
|
||||
)
|
||||
));
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"submitOpenAiOnboarding",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "主 GPT"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "OpenAI 平台账号"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "sk-test"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "gpt-5.4"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "bad-key")
|
||||
);
|
||||
org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
assertEquals("OpenAI 平台账号登录失败:API Key 无效", ShadowToast.getTextOfLatestToast());
|
||||
assertEquals(1, activity.reloadCount);
|
||||
}
|
||||
|
||||
private static final class TestAiAccountsActivity extends AiAccountsActivity {
|
||||
private int reloadCount = 0;
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
reloadCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DirectExecutorService extends AbstractExecutorService {
|
||||
private boolean shutdown;
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
shutdown = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
shutdown = true;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
command.run();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScriptedBossApiClient extends BossApiClient {
|
||||
private final Map<String, RecordingConnection> connections;
|
||||
|
||||
ScriptedBossApiClient(RecordingConnection... connections) {
|
||||
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
|
||||
this.connections = new HashMap<>();
|
||||
for (RecordingConnection connection : connections) {
|
||||
this.connections.put(connection.getURL().getPath(), connection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpURLConnection openConnection(String path) {
|
||||
RecordingConnection connection = connections.get(path);
|
||||
if (connection == null) {
|
||||
throw new IllegalStateException("Missing scripted connection for " + path);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
String encode(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
void rememberIdentity(JSONObject json) {
|
||||
// JVM 单测不需要落 Android 侧身份缓存。
|
||||
}
|
||||
}
|
||||
|
||||
private static final class RecordingConnection extends HttpURLConnection {
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
|
||||
private final Map<String, String> requestHeaders = new HashMap<>();
|
||||
private final int responseCodeValue;
|
||||
private final String responseBody;
|
||||
private final String errorBody;
|
||||
private String requestMethodValue = "GET";
|
||||
private String contentTypeValue = "";
|
||||
|
||||
RecordingConnection(URL url, int responseCodeValue, String responseBody, String errorBody) {
|
||||
super(url);
|
||||
this.responseCodeValue = responseCodeValue;
|
||||
this.responseBody = responseBody;
|
||||
this.errorBody = errorBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {}
|
||||
|
||||
@Override
|
||||
public boolean usingProxy() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() {}
|
||||
|
||||
@Override
|
||||
public void setRequestMethod(String method) throws ProtocolException {
|
||||
requestMethodValue = method;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRequestProperty(String key, String value) {
|
||||
requestHeaders.put(key, value);
|
||||
if ("Content-Type".equalsIgnoreCase(key)) {
|
||||
contentTypeValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getResponseCode() {
|
||||
return responseCodeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getErrorStream() {
|
||||
return new ByteArrayInputStream(errorBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> getHeaderFields() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemorySharedPreferences implements SharedPreferences {
|
||||
private final Map<String, String> values = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return Collections.unmodifiableMap(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
return values.getOrDefault(key, defValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getStringSet(String key, Set<String> defValues) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return new Editor() {
|
||||
@Override
|
||||
public Editor putString(String key, String value) {
|
||||
values.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor remove(String key) {
|
||||
values.remove(key);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor clear() {
|
||||
values.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putStringSet(String key, Set<String> values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putInt(String key, int value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putLong(String key, long value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putFloat(String key, float value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor putBoolean(String key, boolean value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commit() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply() {}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.hyzq.boss;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.view.Gravity;
|
||||
@@ -226,6 +227,103 @@ public class ProjectDetailActivityUiTest {
|
||||
assertFalse(viewTreeContainsText(attachmentView, "你 · 09:26"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingDispatchPlanViewShowsConfirmAndRejectActions() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-group")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "协作群");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.setField(activity, "projectCollaborationMode", "approval_required");
|
||||
ReflectionHelpers.setField(activity, "projectApprovalState", "pending_user");
|
||||
|
||||
JSONObject dispatchPlan = new JSONObject()
|
||||
.put("planId", "dispatch-plan-1")
|
||||
.put("summary", "主 Agent 建议先把任务下发给 Boss 移动控制台。")
|
||||
.put("targets", new JSONArray().put(new JSONObject()
|
||||
.put("projectId", "thread-1")
|
||||
.put("threadDisplayName", "Boss 移动控制台")
|
||||
.put("reason", "最近活跃")));
|
||||
|
||||
View card = ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"buildPendingDispatchPlanView",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, dispatchPlan)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(card, "确认下发"));
|
||||
assertTrue(viewTreeContainsText(card, "拒绝"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderProjectShowsRepairEntryForDirtyGroupAndOpensGroupInfo() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "group-1")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "巡检协作群");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderProject",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildGroupProjectPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONArray.class, null),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildRepairParticipantsPayload())
|
||||
);
|
||||
|
||||
View repairView = findClickableViewContainingText(
|
||||
activity.findViewById(R.id.screen_content),
|
||||
"去修复"
|
||||
);
|
||||
assertNotNull(repairView);
|
||||
assertTrue(viewTreeContainsText(activity.findViewById(R.id.screen_content), "修复群成员"));
|
||||
|
||||
repairView.performClick();
|
||||
|
||||
Intent nextIntent = org.robolectric.Shadows.shadowOf(activity).getNextStartedActivity();
|
||||
assertNotNull(nextIntent);
|
||||
assertEquals(GroupInfoActivity.class.getName(), nextIntent.getComponent().getClassName());
|
||||
assertEquals("group-1", nextIntent.getStringExtra(GroupInfoActivity.EXTRA_PROJECT_ID));
|
||||
}
|
||||
|
||||
private static JSONObject buildGroupProjectPayload() throws Exception {
|
||||
JSONObject threadMeta = new JSONObject()
|
||||
.put("threadId", "group-thread-3")
|
||||
.put("folderName", "Boss");
|
||||
JSONObject project = new JSONObject()
|
||||
.put("id", "group-1")
|
||||
.put("name", "巡检协作群")
|
||||
.put("isGroup", true)
|
||||
.put("collaborationMode", "approval_required")
|
||||
.put("approvalState", "pending_user")
|
||||
.put("threadMeta", threadMeta)
|
||||
.put("messages", new JSONArray());
|
||||
return new JSONObject().put("project", project);
|
||||
}
|
||||
|
||||
private static JSONObject buildRepairParticipantsPayload() throws Exception {
|
||||
return new JSONObject()
|
||||
.put("participants", new JSONArray()
|
||||
.put(new JSONObject()
|
||||
.put("projectId", "master-agent")
|
||||
.put("threadDisplayName", "主 Agent 汇总")
|
||||
.put("folderName", "主控线程")
|
||||
.put("deviceId", "Mac Studio")
|
||||
.put("threadId", "master-agent-thread")
|
||||
.put("status", "invalid_target")
|
||||
.put("statusLabel", "不是可下发线程")
|
||||
.put("canOpenProject", true)))
|
||||
.put("repairRequired", true)
|
||||
.put("repairReason", "当前群聊里有失效或不可下发的线程引用,请重新整理群成员。")
|
||||
.put("validParticipantCount", 0)
|
||||
.put("invalidParticipantCount", 1);
|
||||
}
|
||||
|
||||
private static View buildBoundMessageView(TestProjectDetailActivity activity, String messageId, String body) {
|
||||
TextView messageView = new TextView(activity);
|
||||
messageView.setText(body);
|
||||
@@ -260,6 +358,26 @@ public class ProjectDetailActivityUiTest {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static View findClickableViewContainingText(View root, String expectedText) {
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
if (viewTreeContainsText(root, expectedText) && root.isClickable()) {
|
||||
return root;
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return null;
|
||||
}
|
||||
ViewGroup group = (ViewGroup) root;
|
||||
for (int index = 0; index < group.getChildCount(); index += 1) {
|
||||
View match = findClickableViewContainingText(group.getChildAt(index), expectedText);
|
||||
if (match != null) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class TestProjectDetailActivity extends ProjectDetailActivity {
|
||||
@Override
|
||||
boolean shouldLoadOnCreate() {
|
||||
|
||||
Reference in New Issue
Block a user