@bbearai/react-native 0.3.11 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +38 -3
- package/dist/index.d.ts +38 -3
- package/dist/index.js +201 -93
- package/dist/index.mjs +198 -92
- package/package.json +4 -4
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { ReactNode } from 'react';
|
|
2
|
-
import
|
|
1
|
+
import React, { ReactNode, Component, ErrorInfo } from 'react';
|
|
2
|
+
import * as _bbearai_core from '@bbearai/core';
|
|
3
|
+
import { BugBearConfig, BugBearClient, TesterInfo, TestAssignment, DeviceInfo, TesterThread, TesterMessage, QASession, QAFinding, StartSessionOptions, AddFindingOptions, TesterProfileUpdate, AppContext, captureError } from '@bbearai/core';
|
|
3
4
|
export { AppContext, BugBearConfig, BugBearReport, DeviceInfo, MessageSenderType, ReportType, Severity, TestAssignment, TesterInfo, TesterMessage, TesterThread, ThreadPriority, ThreadType } from '@bbearai/core';
|
|
4
5
|
|
|
5
6
|
interface BugBearContextValue {
|
|
@@ -74,6 +75,8 @@ interface BugBearContextValue {
|
|
|
74
75
|
refreshTesterInfo: () => Promise<void>;
|
|
75
76
|
/** URL to the BugBear web dashboard (for linking testers to the full web experience) */
|
|
76
77
|
dashboardUrl?: string;
|
|
78
|
+
/** Error handler from config — wire to your error reporting service */
|
|
79
|
+
onError?: (error: Error, context?: Record<string, unknown>) => void;
|
|
77
80
|
}
|
|
78
81
|
declare function useBugBear(): BugBearContextValue;
|
|
79
82
|
interface BugBearProviderProps {
|
|
@@ -110,4 +113,36 @@ interface BugBearButtonProps {
|
|
|
110
113
|
}
|
|
111
114
|
declare function BugBearButton({ position, buttonStyle, draggable, initialX, initialY, minY, maxYOffset, }: BugBearButtonProps): React.JSX.Element | null;
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
interface Props {
|
|
117
|
+
children: ReactNode;
|
|
118
|
+
/** Fallback UI to show when an error occurs */
|
|
119
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
120
|
+
/** Called when an error is captured (React ErrorInfo included) */
|
|
121
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
122
|
+
/** Error reporter from BugBearConfig — wire to Sentry.captureException etc. */
|
|
123
|
+
errorReporter?: (error: Error, context?: Record<string, unknown>) => void;
|
|
124
|
+
}
|
|
125
|
+
interface State {
|
|
126
|
+
hasError: boolean;
|
|
127
|
+
error: Error | null;
|
|
128
|
+
errorInfo: ErrorInfo | null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Error boundary that captures React Native errors and integrates with BugBear
|
|
132
|
+
*/
|
|
133
|
+
declare class BugBearErrorBoundary extends Component<Props, State> {
|
|
134
|
+
constructor(props: Props);
|
|
135
|
+
static getDerivedStateFromError(error: Error): Partial<State>;
|
|
136
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
137
|
+
reset: () => void;
|
|
138
|
+
render(): ReactNode;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Hook to get current error context for manual error reporting
|
|
142
|
+
*/
|
|
143
|
+
declare function useErrorContext(): {
|
|
144
|
+
captureError: typeof captureError;
|
|
145
|
+
getEnhancedContext: () => _bbearai_core.EnhancedBugContext;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export { BugBearButton, BugBearErrorBoundary, BugBearProvider, useBugBear, useErrorContext };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import React, { ReactNode } from 'react';
|
|
2
|
-
import
|
|
1
|
+
import React, { ReactNode, Component, ErrorInfo } from 'react';
|
|
2
|
+
import * as _bbearai_core from '@bbearai/core';
|
|
3
|
+
import { BugBearConfig, BugBearClient, TesterInfo, TestAssignment, DeviceInfo, TesterThread, TesterMessage, QASession, QAFinding, StartSessionOptions, AddFindingOptions, TesterProfileUpdate, AppContext, captureError } from '@bbearai/core';
|
|
3
4
|
export { AppContext, BugBearConfig, BugBearReport, DeviceInfo, MessageSenderType, ReportType, Severity, TestAssignment, TesterInfo, TesterMessage, TesterThread, ThreadPriority, ThreadType } from '@bbearai/core';
|
|
4
5
|
|
|
5
6
|
interface BugBearContextValue {
|
|
@@ -74,6 +75,8 @@ interface BugBearContextValue {
|
|
|
74
75
|
refreshTesterInfo: () => Promise<void>;
|
|
75
76
|
/** URL to the BugBear web dashboard (for linking testers to the full web experience) */
|
|
76
77
|
dashboardUrl?: string;
|
|
78
|
+
/** Error handler from config — wire to your error reporting service */
|
|
79
|
+
onError?: (error: Error, context?: Record<string, unknown>) => void;
|
|
77
80
|
}
|
|
78
81
|
declare function useBugBear(): BugBearContextValue;
|
|
79
82
|
interface BugBearProviderProps {
|
|
@@ -110,4 +113,36 @@ interface BugBearButtonProps {
|
|
|
110
113
|
}
|
|
111
114
|
declare function BugBearButton({ position, buttonStyle, draggable, initialX, initialY, minY, maxYOffset, }: BugBearButtonProps): React.JSX.Element | null;
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
interface Props {
|
|
117
|
+
children: ReactNode;
|
|
118
|
+
/** Fallback UI to show when an error occurs */
|
|
119
|
+
fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
|
|
120
|
+
/** Called when an error is captured (React ErrorInfo included) */
|
|
121
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
122
|
+
/** Error reporter from BugBearConfig — wire to Sentry.captureException etc. */
|
|
123
|
+
errorReporter?: (error: Error, context?: Record<string, unknown>) => void;
|
|
124
|
+
}
|
|
125
|
+
interface State {
|
|
126
|
+
hasError: boolean;
|
|
127
|
+
error: Error | null;
|
|
128
|
+
errorInfo: ErrorInfo | null;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Error boundary that captures React Native errors and integrates with BugBear
|
|
132
|
+
*/
|
|
133
|
+
declare class BugBearErrorBoundary extends Component<Props, State> {
|
|
134
|
+
constructor(props: Props);
|
|
135
|
+
static getDerivedStateFromError(error: Error): Partial<State>;
|
|
136
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
137
|
+
reset: () => void;
|
|
138
|
+
render(): ReactNode;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Hook to get current error context for manual error reporting
|
|
142
|
+
*/
|
|
143
|
+
declare function useErrorContext(): {
|
|
144
|
+
captureError: typeof captureError;
|
|
145
|
+
getEnhancedContext: () => _bbearai_core.EnhancedBugContext;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export { BugBearButton, BugBearErrorBoundary, BugBearProvider, useBugBear, useErrorContext };
|
package/dist/index.js
CHANGED
|
@@ -31,8 +31,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BugBearButton: () => BugBearButton,
|
|
34
|
+
BugBearErrorBoundary: () => BugBearErrorBoundary,
|
|
34
35
|
BugBearProvider: () => BugBearProvider,
|
|
35
|
-
useBugBear: () => useBugBear
|
|
36
|
+
useBugBear: () => useBugBear,
|
|
37
|
+
useErrorContext: () => useErrorContext
|
|
36
38
|
});
|
|
37
39
|
module.exports = __toCommonJS(index_exports);
|
|
38
40
|
|
|
@@ -11625,6 +11627,13 @@ var ContextCaptureManager = class {
|
|
|
11625
11627
|
}
|
|
11626
11628
|
};
|
|
11627
11629
|
var contextCapture = new ContextCaptureManager();
|
|
11630
|
+
function captureError(error, errorInfo) {
|
|
11631
|
+
return {
|
|
11632
|
+
errorMessage: error.message,
|
|
11633
|
+
errorStack: error.stack,
|
|
11634
|
+
componentStack: errorInfo?.componentStack
|
|
11635
|
+
};
|
|
11636
|
+
}
|
|
11628
11637
|
var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
|
|
11629
11638
|
var getEnvVar = (key) => {
|
|
11630
11639
|
try {
|
|
@@ -11792,7 +11801,7 @@ var BugBearClient = class {
|
|
|
11792
11801
|
sort_order
|
|
11793
11802
|
)
|
|
11794
11803
|
)
|
|
11795
|
-
`).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
|
|
11804
|
+
`).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
|
|
11796
11805
|
if (error) {
|
|
11797
11806
|
console.error("BugBear: Failed to fetch assignments", error);
|
|
11798
11807
|
return [];
|
|
@@ -11946,7 +11955,7 @@ var BugBearClient = class {
|
|
|
11946
11955
|
if (options?.testResult) {
|
|
11947
11956
|
updateData.test_result = options.testResult;
|
|
11948
11957
|
}
|
|
11949
|
-
const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
|
|
11958
|
+
const { data: updatedRow, error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId).eq("status", currentAssignment.status).select("id").maybeSingle();
|
|
11950
11959
|
if (error) {
|
|
11951
11960
|
console.error("BugBear: Failed to update assignment status", {
|
|
11952
11961
|
message: error.message,
|
|
@@ -11959,6 +11968,9 @@ var BugBearClient = class {
|
|
|
11959
11968
|
});
|
|
11960
11969
|
return { success: false, error: error.message };
|
|
11961
11970
|
}
|
|
11971
|
+
if (!updatedRow) {
|
|
11972
|
+
return { success: false, error: "Assignment status has changed. Please refresh and try again." };
|
|
11973
|
+
}
|
|
11962
11974
|
if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
|
|
11963
11975
|
const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
|
|
11964
11976
|
if (fetchError2) {
|
|
@@ -12185,6 +12197,28 @@ var BugBearClient = class {
|
|
|
12185
12197
|
return null;
|
|
12186
12198
|
}
|
|
12187
12199
|
}
|
|
12200
|
+
/**
|
|
12201
|
+
* Get detailed assignment stats for the current tester via RPC.
|
|
12202
|
+
* Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
|
|
12203
|
+
*/
|
|
12204
|
+
async getTesterStats() {
|
|
12205
|
+
try {
|
|
12206
|
+
const testerInfo = await this.getTesterInfo();
|
|
12207
|
+
if (!testerInfo) return null;
|
|
12208
|
+
const { data, error } = await this.supabase.rpc("get_tester_stats", {
|
|
12209
|
+
p_project_id: this.config.projectId,
|
|
12210
|
+
p_tester_id: testerInfo.id
|
|
12211
|
+
});
|
|
12212
|
+
if (error) {
|
|
12213
|
+
console.error("BugBear: Failed to fetch tester stats", error);
|
|
12214
|
+
return null;
|
|
12215
|
+
}
|
|
12216
|
+
return data;
|
|
12217
|
+
} catch (err) {
|
|
12218
|
+
console.error("BugBear: Error fetching tester stats", err);
|
|
12219
|
+
return null;
|
|
12220
|
+
}
|
|
12221
|
+
}
|
|
12188
12222
|
/**
|
|
12189
12223
|
* Basic email format validation (defense in depth)
|
|
12190
12224
|
*/
|
|
@@ -12518,75 +12552,35 @@ var BugBearClient = class {
|
|
|
12518
12552
|
try {
|
|
12519
12553
|
const testerInfo = await this.getTesterInfo();
|
|
12520
12554
|
if (!testerInfo) return [];
|
|
12521
|
-
const { data
|
|
12522
|
-
|
|
12523
|
-
|
|
12524
|
-
|
|
12525
|
-
priority,
|
|
12526
|
-
is_pinned,
|
|
12527
|
-
is_resolved,
|
|
12528
|
-
last_message_at,
|
|
12529
|
-
created_at
|
|
12530
|
-
`).eq("project_id", this.config.projectId).or(`audience.eq.all,audience_tester_ids.cs.{${testerInfo.id}}`).order("is_pinned", { ascending: false }).order("last_message_at", { ascending: false });
|
|
12555
|
+
const { data, error } = await this.supabase.rpc("get_threads_with_unread", {
|
|
12556
|
+
p_project_id: this.config.projectId,
|
|
12557
|
+
p_tester_id: testerInfo.id
|
|
12558
|
+
});
|
|
12531
12559
|
if (error) {
|
|
12532
|
-
console.error("BugBear: Failed to fetch threads", error);
|
|
12560
|
+
console.error("BugBear: Failed to fetch threads via RPC", error);
|
|
12533
12561
|
return [];
|
|
12534
12562
|
}
|
|
12535
|
-
if (!
|
|
12536
|
-
|
|
12537
|
-
|
|
12538
|
-
|
|
12539
|
-
|
|
12540
|
-
|
|
12541
|
-
|
|
12542
|
-
|
|
12543
|
-
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
12547
|
-
|
|
12548
|
-
|
|
12549
|
-
|
|
12550
|
-
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
}
|
|
12555
|
-
}
|
|
12556
|
-
const unreadCounts = await Promise.all(
|
|
12557
|
-
threads.map(async (thread) => {
|
|
12558
|
-
const readStatus = readStatusMap.get(thread.id);
|
|
12559
|
-
const lastReadAt = readStatus?.last_read_at || "1970-01-01T00:00:00Z";
|
|
12560
|
-
const { count, error: countError } = await this.supabase.from("discussion_messages").select("*", { count: "exact", head: true }).eq("thread_id", thread.id).gt("created_at", lastReadAt);
|
|
12561
|
-
return { threadId: thread.id, count: countError ? 0 : count || 0 };
|
|
12562
|
-
})
|
|
12563
|
-
);
|
|
12564
|
-
const unreadCountMap = new Map(
|
|
12565
|
-
unreadCounts.map((uc) => [uc.threadId, uc.count])
|
|
12566
|
-
);
|
|
12567
|
-
return threads.map((thread) => {
|
|
12568
|
-
const lastMsg = lastMessageMap.get(thread.id);
|
|
12569
|
-
return {
|
|
12570
|
-
id: thread.id,
|
|
12571
|
-
subject: thread.subject,
|
|
12572
|
-
threadType: thread.thread_type,
|
|
12573
|
-
priority: thread.priority,
|
|
12574
|
-
isPinned: thread.is_pinned,
|
|
12575
|
-
isResolved: thread.is_resolved,
|
|
12576
|
-
lastMessageAt: thread.last_message_at,
|
|
12577
|
-
createdAt: thread.created_at,
|
|
12578
|
-
unreadCount: unreadCountMap.get(thread.id) || 0,
|
|
12579
|
-
lastMessage: lastMsg ? {
|
|
12580
|
-
id: lastMsg.id,
|
|
12581
|
-
threadId: lastMsg.thread_id,
|
|
12582
|
-
senderType: lastMsg.sender_type,
|
|
12583
|
-
senderName: lastMsg.sender_name,
|
|
12584
|
-
content: lastMsg.content,
|
|
12585
|
-
createdAt: lastMsg.created_at,
|
|
12586
|
-
attachments: lastMsg.attachments || []
|
|
12587
|
-
} : void 0
|
|
12588
|
-
};
|
|
12589
|
-
});
|
|
12563
|
+
if (!data || data.length === 0) return [];
|
|
12564
|
+
return data.map((row) => ({
|
|
12565
|
+
id: row.thread_id,
|
|
12566
|
+
subject: row.thread_subject,
|
|
12567
|
+
threadType: row.thread_type,
|
|
12568
|
+
priority: row.thread_priority,
|
|
12569
|
+
isPinned: row.is_pinned,
|
|
12570
|
+
isResolved: row.is_resolved,
|
|
12571
|
+
lastMessageAt: row.last_message_at,
|
|
12572
|
+
createdAt: row.created_at,
|
|
12573
|
+
unreadCount: Number(row.unread_count) || 0,
|
|
12574
|
+
lastMessage: row.last_message_preview ? {
|
|
12575
|
+
id: "",
|
|
12576
|
+
threadId: row.thread_id,
|
|
12577
|
+
senderType: row.last_message_sender_type || "system",
|
|
12578
|
+
senderName: row.last_message_sender_name || "",
|
|
12579
|
+
content: row.last_message_preview,
|
|
12580
|
+
createdAt: row.last_message_at,
|
|
12581
|
+
attachments: []
|
|
12582
|
+
} : void 0
|
|
12583
|
+
}));
|
|
12590
12584
|
} catch (err) {
|
|
12591
12585
|
console.error("BugBear: Error fetching threads", err);
|
|
12592
12586
|
return [];
|
|
@@ -12605,7 +12599,7 @@ var BugBearClient = class {
|
|
|
12605
12599
|
content,
|
|
12606
12600
|
created_at,
|
|
12607
12601
|
attachments
|
|
12608
|
-
`).eq("thread_id", threadId).order("created_at", { ascending: true });
|
|
12602
|
+
`).eq("thread_id", threadId).order("created_at", { ascending: true }).limit(200);
|
|
12609
12603
|
if (error) {
|
|
12610
12604
|
console.error("BugBear: Failed to fetch messages", error);
|
|
12611
12605
|
return [];
|
|
@@ -12654,7 +12648,6 @@ var BugBearClient = class {
|
|
|
12654
12648
|
console.error("BugBear: Failed to send message", error);
|
|
12655
12649
|
return false;
|
|
12656
12650
|
}
|
|
12657
|
-
await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
|
|
12658
12651
|
await this.markThreadAsRead(threadId);
|
|
12659
12652
|
return true;
|
|
12660
12653
|
} catch (err) {
|
|
@@ -12885,7 +12878,7 @@ var BugBearClient = class {
|
|
|
12885
12878
|
*/
|
|
12886
12879
|
async getSessionFindings(sessionId) {
|
|
12887
12880
|
try {
|
|
12888
|
-
const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
|
|
12881
|
+
const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
|
|
12889
12882
|
if (error) {
|
|
12890
12883
|
console.error("BugBear: Failed to fetch findings", error);
|
|
12891
12884
|
return [];
|
|
@@ -13029,7 +13022,8 @@ var BugBearContext = (0, import_react.createContext)({
|
|
|
13029
13022
|
updateTesterProfile: async () => ({ success: false }),
|
|
13030
13023
|
refreshTesterInfo: async () => {
|
|
13031
13024
|
},
|
|
13032
|
-
dashboardUrl: void 0
|
|
13025
|
+
dashboardUrl: void 0,
|
|
13026
|
+
onError: void 0
|
|
13033
13027
|
});
|
|
13034
13028
|
function useBugBear() {
|
|
13035
13029
|
return (0, import_react.useContext)(BugBearContext);
|
|
@@ -13178,6 +13172,9 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
|
|
|
13178
13172
|
}
|
|
13179
13173
|
} catch (err) {
|
|
13180
13174
|
console.error("BugBear: Init error", err);
|
|
13175
|
+
if (err instanceof Error) {
|
|
13176
|
+
config.onError?.(err, { phase: "init" });
|
|
13177
|
+
}
|
|
13181
13178
|
} finally {
|
|
13182
13179
|
setIsLoading(false);
|
|
13183
13180
|
}
|
|
@@ -13235,7 +13232,8 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
|
|
|
13235
13232
|
refreshTesterStatus,
|
|
13236
13233
|
updateTesterProfile,
|
|
13237
13234
|
refreshTesterInfo,
|
|
13238
|
-
dashboardUrl: config.dashboardUrl
|
|
13235
|
+
dashboardUrl: config.dashboardUrl,
|
|
13236
|
+
onError: config.onError
|
|
13239
13237
|
}
|
|
13240
13238
|
},
|
|
13241
13239
|
children
|
|
@@ -13745,6 +13743,7 @@ function TestDetailScreen({ testId, nav }) {
|
|
|
13745
13743
|
const [selectedSkipReason, setSelectedSkipReason] = (0, import_react4.useState)(null);
|
|
13746
13744
|
const [skipNotes, setSkipNotes] = (0, import_react4.useState)("");
|
|
13747
13745
|
const [skipping, setSkipping] = (0, import_react4.useState)(false);
|
|
13746
|
+
const [isSubmitting, setIsSubmitting] = (0, import_react4.useState)(false);
|
|
13748
13747
|
(0, import_react4.useEffect)(() => {
|
|
13749
13748
|
setCriteriaResults({});
|
|
13750
13749
|
setShowSteps(true);
|
|
@@ -13767,26 +13766,36 @@ function TestDetailScreen({ testId, nav }) {
|
|
|
13767
13766
|
const allTests = assignments;
|
|
13768
13767
|
const currentIndex = displayedAssignment ? assignments.indexOf(displayedAssignment) : -1;
|
|
13769
13768
|
const handlePass = (0, import_react4.useCallback)(async () => {
|
|
13770
|
-
if (!client || !displayedAssignment) return;
|
|
13769
|
+
if (!client || !displayedAssignment || isSubmitting) return;
|
|
13771
13770
|
import_react_native4.Keyboard.dismiss();
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13771
|
+
setIsSubmitting(true);
|
|
13772
|
+
try {
|
|
13773
|
+
await client.passAssignment(displayedAssignment.id);
|
|
13774
|
+
await refreshAssignments();
|
|
13775
|
+
nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
|
|
13776
|
+
} finally {
|
|
13777
|
+
setIsSubmitting(false);
|
|
13778
|
+
}
|
|
13779
|
+
}, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
|
|
13776
13780
|
const handleFail = (0, import_react4.useCallback)(async () => {
|
|
13777
|
-
if (!client || !displayedAssignment) return;
|
|
13781
|
+
if (!client || !displayedAssignment || isSubmitting) return;
|
|
13778
13782
|
import_react_native4.Keyboard.dismiss();
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
13782
|
-
|
|
13783
|
-
|
|
13784
|
-
|
|
13785
|
-
|
|
13786
|
-
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13783
|
+
setIsSubmitting(true);
|
|
13784
|
+
try {
|
|
13785
|
+
await client.failAssignment(displayedAssignment.id);
|
|
13786
|
+
await refreshAssignments();
|
|
13787
|
+
nav.replace({
|
|
13788
|
+
name: "REPORT",
|
|
13789
|
+
prefill: {
|
|
13790
|
+
type: "test_fail",
|
|
13791
|
+
assignmentId: displayedAssignment.id,
|
|
13792
|
+
testCaseId: displayedAssignment.testCase.id
|
|
13793
|
+
}
|
|
13794
|
+
});
|
|
13795
|
+
} finally {
|
|
13796
|
+
setIsSubmitting(false);
|
|
13797
|
+
}
|
|
13798
|
+
}, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
|
|
13790
13799
|
const handleSkip = (0, import_react4.useCallback)(async () => {
|
|
13791
13800
|
if (!client || !displayedAssignment || !selectedSkipReason) return;
|
|
13792
13801
|
import_react_native4.Keyboard.dismiss();
|
|
@@ -13867,7 +13876,7 @@ function TestDetailScreen({ testId, nav }) {
|
|
|
13867
13876
|
}
|
|
13868
13877
|
},
|
|
13869
13878
|
/* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.navigateText }, "\u{1F9ED} Go to test location")
|
|
13870
|
-
), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.folderProgress }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.actionButtons }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.failBtn], onPress: handleFail }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.failBtnText }, "Fail")), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.skipBtn], onPress: () => setShowSkipModal(true) }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.passBtn], onPress: handlePass }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.passBtnText }, "Pass"))), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.modalOverlay }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.modalContent }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.modalSubtitle }, "Select a reason:"), [
|
|
13879
|
+
), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.folderProgress }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.actionButtons }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.failBtnText }, isSubmitting ? "Failing..." : "Fail")), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.passBtnText }, isSubmitting ? "Passing..." : "Pass"))), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.modalOverlay }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.modalContent }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.modalSubtitle }, "Select a reason:"), [
|
|
13871
13880
|
{ reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
|
|
13872
13881
|
{ reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
|
|
13873
13882
|
{ reason: "dependency", label: "\u{1F517} Needs another test first" },
|
|
@@ -15261,9 +15270,108 @@ var styles13 = import_react_native15.StyleSheet.create({
|
|
|
15261
15270
|
fontSize: 14
|
|
15262
15271
|
}
|
|
15263
15272
|
});
|
|
15273
|
+
|
|
15274
|
+
// src/BugBearErrorBoundary.tsx
|
|
15275
|
+
var import_react17 = __toESM(require("react"));
|
|
15276
|
+
var import_react_native16 = require("react-native");
|
|
15277
|
+
var BugBearErrorBoundary = class extends import_react17.Component {
|
|
15278
|
+
constructor(props) {
|
|
15279
|
+
super(props);
|
|
15280
|
+
this.reset = () => {
|
|
15281
|
+
this.setState({
|
|
15282
|
+
hasError: false,
|
|
15283
|
+
error: null,
|
|
15284
|
+
errorInfo: null
|
|
15285
|
+
});
|
|
15286
|
+
};
|
|
15287
|
+
this.state = {
|
|
15288
|
+
hasError: false,
|
|
15289
|
+
error: null,
|
|
15290
|
+
errorInfo: null
|
|
15291
|
+
};
|
|
15292
|
+
}
|
|
15293
|
+
static getDerivedStateFromError(error) {
|
|
15294
|
+
return { hasError: true, error };
|
|
15295
|
+
}
|
|
15296
|
+
componentDidCatch(error, errorInfo) {
|
|
15297
|
+
this.setState({ errorInfo });
|
|
15298
|
+
captureError(error, {
|
|
15299
|
+
componentStack: errorInfo.componentStack ?? void 0
|
|
15300
|
+
});
|
|
15301
|
+
console.error("BugBear: Error caught by ErrorBoundary", {
|
|
15302
|
+
error: error.message,
|
|
15303
|
+
componentStack: errorInfo.componentStack?.slice(0, 500)
|
|
15304
|
+
});
|
|
15305
|
+
this.props.onError?.(error, errorInfo);
|
|
15306
|
+
this.props.errorReporter?.(error, {
|
|
15307
|
+
componentStack: errorInfo.componentStack ?? void 0,
|
|
15308
|
+
source: "BugBearErrorBoundary"
|
|
15309
|
+
});
|
|
15310
|
+
}
|
|
15311
|
+
render() {
|
|
15312
|
+
const { hasError, error } = this.state;
|
|
15313
|
+
const { children, fallback } = this.props;
|
|
15314
|
+
if (hasError && error) {
|
|
15315
|
+
if (typeof fallback === "function") {
|
|
15316
|
+
return fallback(error, this.reset);
|
|
15317
|
+
}
|
|
15318
|
+
if (fallback) {
|
|
15319
|
+
return fallback;
|
|
15320
|
+
}
|
|
15321
|
+
return /* @__PURE__ */ import_react17.default.createElement(import_react_native16.View, { style: styles14.container }, /* @__PURE__ */ import_react17.default.createElement(import_react_native16.Text, { style: styles14.title }, "Something went wrong"), /* @__PURE__ */ import_react17.default.createElement(import_react_native16.Text, { style: styles14.message }, error.message), /* @__PURE__ */ import_react17.default.createElement(import_react_native16.TouchableOpacity, { style: styles14.button, onPress: this.reset }, /* @__PURE__ */ import_react17.default.createElement(import_react_native16.Text, { style: styles14.buttonText }, "Try Again")), /* @__PURE__ */ import_react17.default.createElement(import_react_native16.Text, { style: styles14.caption }, "The error has been captured by BugBear"));
|
|
15322
|
+
}
|
|
15323
|
+
return children;
|
|
15324
|
+
}
|
|
15325
|
+
};
|
|
15326
|
+
function useErrorContext() {
|
|
15327
|
+
return {
|
|
15328
|
+
captureError,
|
|
15329
|
+
getEnhancedContext: () => contextCapture.getEnhancedContext()
|
|
15330
|
+
};
|
|
15331
|
+
}
|
|
15332
|
+
var styles14 = import_react_native16.StyleSheet.create({
|
|
15333
|
+
container: {
|
|
15334
|
+
padding: 20,
|
|
15335
|
+
margin: 20,
|
|
15336
|
+
backgroundColor: "#fef2f2",
|
|
15337
|
+
borderWidth: 1,
|
|
15338
|
+
borderColor: "#fecaca",
|
|
15339
|
+
borderRadius: 8
|
|
15340
|
+
},
|
|
15341
|
+
title: {
|
|
15342
|
+
fontSize: 16,
|
|
15343
|
+
fontWeight: "600",
|
|
15344
|
+
color: "#991b1b",
|
|
15345
|
+
marginBottom: 8
|
|
15346
|
+
},
|
|
15347
|
+
message: {
|
|
15348
|
+
fontSize: 14,
|
|
15349
|
+
color: "#7f1d1d",
|
|
15350
|
+
marginBottom: 12
|
|
15351
|
+
},
|
|
15352
|
+
button: {
|
|
15353
|
+
backgroundColor: "#dc2626",
|
|
15354
|
+
paddingHorizontal: 16,
|
|
15355
|
+
paddingVertical: 8,
|
|
15356
|
+
borderRadius: 6,
|
|
15357
|
+
alignSelf: "flex-start"
|
|
15358
|
+
},
|
|
15359
|
+
buttonText: {
|
|
15360
|
+
color: "white",
|
|
15361
|
+
fontSize: 14,
|
|
15362
|
+
fontWeight: "500"
|
|
15363
|
+
},
|
|
15364
|
+
caption: {
|
|
15365
|
+
fontSize: 12,
|
|
15366
|
+
color: "#9ca3af",
|
|
15367
|
+
marginTop: 12
|
|
15368
|
+
}
|
|
15369
|
+
});
|
|
15264
15370
|
// Annotate the CommonJS export names for ESM import in node:
|
|
15265
15371
|
0 && (module.exports = {
|
|
15266
15372
|
BugBearButton,
|
|
15373
|
+
BugBearErrorBoundary,
|
|
15267
15374
|
BugBearProvider,
|
|
15268
|
-
useBugBear
|
|
15375
|
+
useBugBear,
|
|
15376
|
+
useErrorContext
|
|
15269
15377
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -11594,6 +11594,13 @@ var ContextCaptureManager = class {
|
|
|
11594
11594
|
}
|
|
11595
11595
|
};
|
|
11596
11596
|
var contextCapture = new ContextCaptureManager();
|
|
11597
|
+
function captureError(error, errorInfo) {
|
|
11598
|
+
return {
|
|
11599
|
+
errorMessage: error.message,
|
|
11600
|
+
errorStack: error.stack,
|
|
11601
|
+
componentStack: errorInfo?.componentStack
|
|
11602
|
+
};
|
|
11603
|
+
}
|
|
11597
11604
|
var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
|
|
11598
11605
|
var getEnvVar = (key) => {
|
|
11599
11606
|
try {
|
|
@@ -11761,7 +11768,7 @@ var BugBearClient = class {
|
|
|
11761
11768
|
sort_order
|
|
11762
11769
|
)
|
|
11763
11770
|
)
|
|
11764
|
-
`).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
|
|
11771
|
+
`).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
|
|
11765
11772
|
if (error) {
|
|
11766
11773
|
console.error("BugBear: Failed to fetch assignments", error);
|
|
11767
11774
|
return [];
|
|
@@ -11915,7 +11922,7 @@ var BugBearClient = class {
|
|
|
11915
11922
|
if (options?.testResult) {
|
|
11916
11923
|
updateData.test_result = options.testResult;
|
|
11917
11924
|
}
|
|
11918
|
-
const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
|
|
11925
|
+
const { data: updatedRow, error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId).eq("status", currentAssignment.status).select("id").maybeSingle();
|
|
11919
11926
|
if (error) {
|
|
11920
11927
|
console.error("BugBear: Failed to update assignment status", {
|
|
11921
11928
|
message: error.message,
|
|
@@ -11928,6 +11935,9 @@ var BugBearClient = class {
|
|
|
11928
11935
|
});
|
|
11929
11936
|
return { success: false, error: error.message };
|
|
11930
11937
|
}
|
|
11938
|
+
if (!updatedRow) {
|
|
11939
|
+
return { success: false, error: "Assignment status has changed. Please refresh and try again." };
|
|
11940
|
+
}
|
|
11931
11941
|
if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
|
|
11932
11942
|
const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
|
|
11933
11943
|
if (fetchError2) {
|
|
@@ -12154,6 +12164,28 @@ var BugBearClient = class {
|
|
|
12154
12164
|
return null;
|
|
12155
12165
|
}
|
|
12156
12166
|
}
|
|
12167
|
+
/**
|
|
12168
|
+
* Get detailed assignment stats for the current tester via RPC.
|
|
12169
|
+
* Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
|
|
12170
|
+
*/
|
|
12171
|
+
async getTesterStats() {
|
|
12172
|
+
try {
|
|
12173
|
+
const testerInfo = await this.getTesterInfo();
|
|
12174
|
+
if (!testerInfo) return null;
|
|
12175
|
+
const { data, error } = await this.supabase.rpc("get_tester_stats", {
|
|
12176
|
+
p_project_id: this.config.projectId,
|
|
12177
|
+
p_tester_id: testerInfo.id
|
|
12178
|
+
});
|
|
12179
|
+
if (error) {
|
|
12180
|
+
console.error("BugBear: Failed to fetch tester stats", error);
|
|
12181
|
+
return null;
|
|
12182
|
+
}
|
|
12183
|
+
return data;
|
|
12184
|
+
} catch (err) {
|
|
12185
|
+
console.error("BugBear: Error fetching tester stats", err);
|
|
12186
|
+
return null;
|
|
12187
|
+
}
|
|
12188
|
+
}
|
|
12157
12189
|
/**
|
|
12158
12190
|
* Basic email format validation (defense in depth)
|
|
12159
12191
|
*/
|
|
@@ -12487,75 +12519,35 @@ var BugBearClient = class {
|
|
|
12487
12519
|
try {
|
|
12488
12520
|
const testerInfo = await this.getTesterInfo();
|
|
12489
12521
|
if (!testerInfo) return [];
|
|
12490
|
-
const { data
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
priority,
|
|
12495
|
-
is_pinned,
|
|
12496
|
-
is_resolved,
|
|
12497
|
-
last_message_at,
|
|
12498
|
-
created_at
|
|
12499
|
-
`).eq("project_id", this.config.projectId).or(`audience.eq.all,audience_tester_ids.cs.{${testerInfo.id}}`).order("is_pinned", { ascending: false }).order("last_message_at", { ascending: false });
|
|
12522
|
+
const { data, error } = await this.supabase.rpc("get_threads_with_unread", {
|
|
12523
|
+
p_project_id: this.config.projectId,
|
|
12524
|
+
p_tester_id: testerInfo.id
|
|
12525
|
+
});
|
|
12500
12526
|
if (error) {
|
|
12501
|
-
console.error("BugBear: Failed to fetch threads", error);
|
|
12527
|
+
console.error("BugBear: Failed to fetch threads via RPC", error);
|
|
12502
12528
|
return [];
|
|
12503
12529
|
}
|
|
12504
|
-
if (!
|
|
12505
|
-
|
|
12506
|
-
|
|
12507
|
-
|
|
12508
|
-
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
|
|
12518
|
-
|
|
12519
|
-
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
|
|
12523
|
-
}
|
|
12524
|
-
}
|
|
12525
|
-
const unreadCounts = await Promise.all(
|
|
12526
|
-
threads.map(async (thread) => {
|
|
12527
|
-
const readStatus = readStatusMap.get(thread.id);
|
|
12528
|
-
const lastReadAt = readStatus?.last_read_at || "1970-01-01T00:00:00Z";
|
|
12529
|
-
const { count, error: countError } = await this.supabase.from("discussion_messages").select("*", { count: "exact", head: true }).eq("thread_id", thread.id).gt("created_at", lastReadAt);
|
|
12530
|
-
return { threadId: thread.id, count: countError ? 0 : count || 0 };
|
|
12531
|
-
})
|
|
12532
|
-
);
|
|
12533
|
-
const unreadCountMap = new Map(
|
|
12534
|
-
unreadCounts.map((uc) => [uc.threadId, uc.count])
|
|
12535
|
-
);
|
|
12536
|
-
return threads.map((thread) => {
|
|
12537
|
-
const lastMsg = lastMessageMap.get(thread.id);
|
|
12538
|
-
return {
|
|
12539
|
-
id: thread.id,
|
|
12540
|
-
subject: thread.subject,
|
|
12541
|
-
threadType: thread.thread_type,
|
|
12542
|
-
priority: thread.priority,
|
|
12543
|
-
isPinned: thread.is_pinned,
|
|
12544
|
-
isResolved: thread.is_resolved,
|
|
12545
|
-
lastMessageAt: thread.last_message_at,
|
|
12546
|
-
createdAt: thread.created_at,
|
|
12547
|
-
unreadCount: unreadCountMap.get(thread.id) || 0,
|
|
12548
|
-
lastMessage: lastMsg ? {
|
|
12549
|
-
id: lastMsg.id,
|
|
12550
|
-
threadId: lastMsg.thread_id,
|
|
12551
|
-
senderType: lastMsg.sender_type,
|
|
12552
|
-
senderName: lastMsg.sender_name,
|
|
12553
|
-
content: lastMsg.content,
|
|
12554
|
-
createdAt: lastMsg.created_at,
|
|
12555
|
-
attachments: lastMsg.attachments || []
|
|
12556
|
-
} : void 0
|
|
12557
|
-
};
|
|
12558
|
-
});
|
|
12530
|
+
if (!data || data.length === 0) return [];
|
|
12531
|
+
return data.map((row) => ({
|
|
12532
|
+
id: row.thread_id,
|
|
12533
|
+
subject: row.thread_subject,
|
|
12534
|
+
threadType: row.thread_type,
|
|
12535
|
+
priority: row.thread_priority,
|
|
12536
|
+
isPinned: row.is_pinned,
|
|
12537
|
+
isResolved: row.is_resolved,
|
|
12538
|
+
lastMessageAt: row.last_message_at,
|
|
12539
|
+
createdAt: row.created_at,
|
|
12540
|
+
unreadCount: Number(row.unread_count) || 0,
|
|
12541
|
+
lastMessage: row.last_message_preview ? {
|
|
12542
|
+
id: "",
|
|
12543
|
+
threadId: row.thread_id,
|
|
12544
|
+
senderType: row.last_message_sender_type || "system",
|
|
12545
|
+
senderName: row.last_message_sender_name || "",
|
|
12546
|
+
content: row.last_message_preview,
|
|
12547
|
+
createdAt: row.last_message_at,
|
|
12548
|
+
attachments: []
|
|
12549
|
+
} : void 0
|
|
12550
|
+
}));
|
|
12559
12551
|
} catch (err) {
|
|
12560
12552
|
console.error("BugBear: Error fetching threads", err);
|
|
12561
12553
|
return [];
|
|
@@ -12574,7 +12566,7 @@ var BugBearClient = class {
|
|
|
12574
12566
|
content,
|
|
12575
12567
|
created_at,
|
|
12576
12568
|
attachments
|
|
12577
|
-
`).eq("thread_id", threadId).order("created_at", { ascending: true });
|
|
12569
|
+
`).eq("thread_id", threadId).order("created_at", { ascending: true }).limit(200);
|
|
12578
12570
|
if (error) {
|
|
12579
12571
|
console.error("BugBear: Failed to fetch messages", error);
|
|
12580
12572
|
return [];
|
|
@@ -12623,7 +12615,6 @@ var BugBearClient = class {
|
|
|
12623
12615
|
console.error("BugBear: Failed to send message", error);
|
|
12624
12616
|
return false;
|
|
12625
12617
|
}
|
|
12626
|
-
await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
|
|
12627
12618
|
await this.markThreadAsRead(threadId);
|
|
12628
12619
|
return true;
|
|
12629
12620
|
} catch (err) {
|
|
@@ -12854,7 +12845,7 @@ var BugBearClient = class {
|
|
|
12854
12845
|
*/
|
|
12855
12846
|
async getSessionFindings(sessionId) {
|
|
12856
12847
|
try {
|
|
12857
|
-
const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
|
|
12848
|
+
const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
|
|
12858
12849
|
if (error) {
|
|
12859
12850
|
console.error("BugBear: Failed to fetch findings", error);
|
|
12860
12851
|
return [];
|
|
@@ -12998,7 +12989,8 @@ var BugBearContext = createContext({
|
|
|
12998
12989
|
updateTesterProfile: async () => ({ success: false }),
|
|
12999
12990
|
refreshTesterInfo: async () => {
|
|
13000
12991
|
},
|
|
13001
|
-
dashboardUrl: void 0
|
|
12992
|
+
dashboardUrl: void 0,
|
|
12993
|
+
onError: void 0
|
|
13002
12994
|
});
|
|
13003
12995
|
function useBugBear() {
|
|
13004
12996
|
return useContext(BugBearContext);
|
|
@@ -13147,6 +13139,9 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
|
|
|
13147
13139
|
}
|
|
13148
13140
|
} catch (err) {
|
|
13149
13141
|
console.error("BugBear: Init error", err);
|
|
13142
|
+
if (err instanceof Error) {
|
|
13143
|
+
config.onError?.(err, { phase: "init" });
|
|
13144
|
+
}
|
|
13150
13145
|
} finally {
|
|
13151
13146
|
setIsLoading(false);
|
|
13152
13147
|
}
|
|
@@ -13204,7 +13199,8 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
|
|
|
13204
13199
|
refreshTesterStatus,
|
|
13205
13200
|
updateTesterProfile,
|
|
13206
13201
|
refreshTesterInfo,
|
|
13207
|
-
dashboardUrl: config.dashboardUrl
|
|
13202
|
+
dashboardUrl: config.dashboardUrl,
|
|
13203
|
+
onError: config.onError
|
|
13208
13204
|
}
|
|
13209
13205
|
},
|
|
13210
13206
|
children
|
|
@@ -13729,6 +13725,7 @@ function TestDetailScreen({ testId, nav }) {
|
|
|
13729
13725
|
const [selectedSkipReason, setSelectedSkipReason] = useState2(null);
|
|
13730
13726
|
const [skipNotes, setSkipNotes] = useState2("");
|
|
13731
13727
|
const [skipping, setSkipping] = useState2(false);
|
|
13728
|
+
const [isSubmitting, setIsSubmitting] = useState2(false);
|
|
13732
13729
|
useEffect3(() => {
|
|
13733
13730
|
setCriteriaResults({});
|
|
13734
13731
|
setShowSteps(true);
|
|
@@ -13751,26 +13748,36 @@ function TestDetailScreen({ testId, nav }) {
|
|
|
13751
13748
|
const allTests = assignments;
|
|
13752
13749
|
const currentIndex = displayedAssignment ? assignments.indexOf(displayedAssignment) : -1;
|
|
13753
13750
|
const handlePass = useCallback2(async () => {
|
|
13754
|
-
if (!client || !displayedAssignment) return;
|
|
13751
|
+
if (!client || !displayedAssignment || isSubmitting) return;
|
|
13755
13752
|
Keyboard.dismiss();
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
|
|
13759
|
-
|
|
13753
|
+
setIsSubmitting(true);
|
|
13754
|
+
try {
|
|
13755
|
+
await client.passAssignment(displayedAssignment.id);
|
|
13756
|
+
await refreshAssignments();
|
|
13757
|
+
nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
|
|
13758
|
+
} finally {
|
|
13759
|
+
setIsSubmitting(false);
|
|
13760
|
+
}
|
|
13761
|
+
}, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
|
|
13760
13762
|
const handleFail = useCallback2(async () => {
|
|
13761
|
-
if (!client || !displayedAssignment) return;
|
|
13763
|
+
if (!client || !displayedAssignment || isSubmitting) return;
|
|
13762
13764
|
Keyboard.dismiss();
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13765
|
+
setIsSubmitting(true);
|
|
13766
|
+
try {
|
|
13767
|
+
await client.failAssignment(displayedAssignment.id);
|
|
13768
|
+
await refreshAssignments();
|
|
13769
|
+
nav.replace({
|
|
13770
|
+
name: "REPORT",
|
|
13771
|
+
prefill: {
|
|
13772
|
+
type: "test_fail",
|
|
13773
|
+
assignmentId: displayedAssignment.id,
|
|
13774
|
+
testCaseId: displayedAssignment.testCase.id
|
|
13775
|
+
}
|
|
13776
|
+
});
|
|
13777
|
+
} finally {
|
|
13778
|
+
setIsSubmitting(false);
|
|
13779
|
+
}
|
|
13780
|
+
}, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
|
|
13774
13781
|
const handleSkip = useCallback2(async () => {
|
|
13775
13782
|
if (!client || !displayedAssignment || !selectedSkipReason) return;
|
|
13776
13783
|
Keyboard.dismiss();
|
|
@@ -13851,7 +13858,7 @@ function TestDetailScreen({ testId, nav }) {
|
|
|
13851
13858
|
}
|
|
13852
13859
|
},
|
|
13853
13860
|
/* @__PURE__ */ React3.createElement(Text2, { style: styles2.navigateText }, "\u{1F9ED} Go to test location")
|
|
13854
|
-
), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ React3.createElement(View2, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ React3.createElement(View2, { style: styles2.folderProgress }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ React3.createElement(View2, { style: styles2.actionButtons }, /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.failBtn], onPress: handleFail }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.failBtnText }, "Fail")), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.skipBtn], onPress: () => setShowSkipModal(true) }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.passBtn], onPress: handlePass }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.passBtnText }, "Pass"))), /* @__PURE__ */ React3.createElement(Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ React3.createElement(View2, { style: styles2.modalOverlay }, /* @__PURE__ */ React3.createElement(View2, { style: styles2.modalContent }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ React3.createElement(Text2, { style: styles2.modalSubtitle }, "Select a reason:"), [
|
|
13861
|
+
), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ React3.createElement(View2, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ React3.createElement(View2, { style: styles2.folderProgress }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ React3.createElement(View2, { style: styles2.actionButtons }, /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.failBtnText }, isSubmitting ? "Failing..." : "Fail")), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.passBtnText }, isSubmitting ? "Passing..." : "Pass"))), /* @__PURE__ */ React3.createElement(Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ React3.createElement(View2, { style: styles2.modalOverlay }, /* @__PURE__ */ React3.createElement(View2, { style: styles2.modalContent }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ React3.createElement(Text2, { style: styles2.modalSubtitle }, "Select a reason:"), [
|
|
13855
13862
|
{ reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
|
|
13856
13863
|
{ reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
|
|
13857
13864
|
{ reason: "dependency", label: "\u{1F517} Needs another test first" },
|
|
@@ -15245,8 +15252,107 @@ var styles13 = StyleSheet14.create({
|
|
|
15245
15252
|
fontSize: 14
|
|
15246
15253
|
}
|
|
15247
15254
|
});
|
|
15255
|
+
|
|
15256
|
+
// src/BugBearErrorBoundary.tsx
|
|
15257
|
+
import React15, { Component } from "react";
|
|
15258
|
+
import { View as View14, Text as Text14, TouchableOpacity as TouchableOpacity13, StyleSheet as StyleSheet15 } from "react-native";
|
|
15259
|
+
var BugBearErrorBoundary = class extends Component {
|
|
15260
|
+
constructor(props) {
|
|
15261
|
+
super(props);
|
|
15262
|
+
this.reset = () => {
|
|
15263
|
+
this.setState({
|
|
15264
|
+
hasError: false,
|
|
15265
|
+
error: null,
|
|
15266
|
+
errorInfo: null
|
|
15267
|
+
});
|
|
15268
|
+
};
|
|
15269
|
+
this.state = {
|
|
15270
|
+
hasError: false,
|
|
15271
|
+
error: null,
|
|
15272
|
+
errorInfo: null
|
|
15273
|
+
};
|
|
15274
|
+
}
|
|
15275
|
+
static getDerivedStateFromError(error) {
|
|
15276
|
+
return { hasError: true, error };
|
|
15277
|
+
}
|
|
15278
|
+
componentDidCatch(error, errorInfo) {
|
|
15279
|
+
this.setState({ errorInfo });
|
|
15280
|
+
captureError(error, {
|
|
15281
|
+
componentStack: errorInfo.componentStack ?? void 0
|
|
15282
|
+
});
|
|
15283
|
+
console.error("BugBear: Error caught by ErrorBoundary", {
|
|
15284
|
+
error: error.message,
|
|
15285
|
+
componentStack: errorInfo.componentStack?.slice(0, 500)
|
|
15286
|
+
});
|
|
15287
|
+
this.props.onError?.(error, errorInfo);
|
|
15288
|
+
this.props.errorReporter?.(error, {
|
|
15289
|
+
componentStack: errorInfo.componentStack ?? void 0,
|
|
15290
|
+
source: "BugBearErrorBoundary"
|
|
15291
|
+
});
|
|
15292
|
+
}
|
|
15293
|
+
render() {
|
|
15294
|
+
const { hasError, error } = this.state;
|
|
15295
|
+
const { children, fallback } = this.props;
|
|
15296
|
+
if (hasError && error) {
|
|
15297
|
+
if (typeof fallback === "function") {
|
|
15298
|
+
return fallback(error, this.reset);
|
|
15299
|
+
}
|
|
15300
|
+
if (fallback) {
|
|
15301
|
+
return fallback;
|
|
15302
|
+
}
|
|
15303
|
+
return /* @__PURE__ */ React15.createElement(View14, { style: styles14.container }, /* @__PURE__ */ React15.createElement(Text14, { style: styles14.title }, "Something went wrong"), /* @__PURE__ */ React15.createElement(Text14, { style: styles14.message }, error.message), /* @__PURE__ */ React15.createElement(TouchableOpacity13, { style: styles14.button, onPress: this.reset }, /* @__PURE__ */ React15.createElement(Text14, { style: styles14.buttonText }, "Try Again")), /* @__PURE__ */ React15.createElement(Text14, { style: styles14.caption }, "The error has been captured by BugBear"));
|
|
15304
|
+
}
|
|
15305
|
+
return children;
|
|
15306
|
+
}
|
|
15307
|
+
};
|
|
15308
|
+
function useErrorContext() {
|
|
15309
|
+
return {
|
|
15310
|
+
captureError,
|
|
15311
|
+
getEnhancedContext: () => contextCapture.getEnhancedContext()
|
|
15312
|
+
};
|
|
15313
|
+
}
|
|
15314
|
+
var styles14 = StyleSheet15.create({
|
|
15315
|
+
container: {
|
|
15316
|
+
padding: 20,
|
|
15317
|
+
margin: 20,
|
|
15318
|
+
backgroundColor: "#fef2f2",
|
|
15319
|
+
borderWidth: 1,
|
|
15320
|
+
borderColor: "#fecaca",
|
|
15321
|
+
borderRadius: 8
|
|
15322
|
+
},
|
|
15323
|
+
title: {
|
|
15324
|
+
fontSize: 16,
|
|
15325
|
+
fontWeight: "600",
|
|
15326
|
+
color: "#991b1b",
|
|
15327
|
+
marginBottom: 8
|
|
15328
|
+
},
|
|
15329
|
+
message: {
|
|
15330
|
+
fontSize: 14,
|
|
15331
|
+
color: "#7f1d1d",
|
|
15332
|
+
marginBottom: 12
|
|
15333
|
+
},
|
|
15334
|
+
button: {
|
|
15335
|
+
backgroundColor: "#dc2626",
|
|
15336
|
+
paddingHorizontal: 16,
|
|
15337
|
+
paddingVertical: 8,
|
|
15338
|
+
borderRadius: 6,
|
|
15339
|
+
alignSelf: "flex-start"
|
|
15340
|
+
},
|
|
15341
|
+
buttonText: {
|
|
15342
|
+
color: "white",
|
|
15343
|
+
fontSize: 14,
|
|
15344
|
+
fontWeight: "500"
|
|
15345
|
+
},
|
|
15346
|
+
caption: {
|
|
15347
|
+
fontSize: 12,
|
|
15348
|
+
color: "#9ca3af",
|
|
15349
|
+
marginTop: 12
|
|
15350
|
+
}
|
|
15351
|
+
});
|
|
15248
15352
|
export {
|
|
15249
15353
|
BugBearButton,
|
|
15354
|
+
BugBearErrorBoundary,
|
|
15250
15355
|
BugBearProvider,
|
|
15251
|
-
useBugBear
|
|
15356
|
+
useBugBear,
|
|
15357
|
+
useErrorContext
|
|
15252
15358
|
};
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbearai/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "BugBear React Native components for mobile apps",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
10
11
|
"import": "./dist/index.mjs",
|
|
11
|
-
"require": "./dist/index.js"
|
|
12
|
-
"types": "./dist/index.d.ts"
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@bbearai/core": "^0.
|
|
52
|
+
"@bbearai/core": "^0.3.0",
|
|
53
53
|
"@eslint/js": "^9.39.2",
|
|
54
54
|
"@types/react": "^18.2.0",
|
|
55
55
|
"eslint": "^9.39.2",
|