Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions public/js/staktrak.js
Original file line number Diff line number Diff line change
Expand Up @@ -3573,8 +3573,10 @@ var userBehaviour = (() => {
},
getParentOrigin()
);
return id;
} catch (error) {
console.error(`[Screenshot] Error capturing for actionIndex=${actionIndex}:`, error);
return null;
}
}
function beginReplay(actions, testCode) {
Expand Down Expand Up @@ -3649,20 +3651,26 @@ var userBehaviour = (() => {
executeNextPlaywrightAction();
}, 500);
} catch (error) {
state.errors.push(
`Action ${state.currentActionIndex + 1}: ${error instanceof Error ? error.message : "Unknown error"}`
const message = error instanceof Error ? error.message : "Unknown error";
state.errors.push(`Action ${state.currentActionIndex + 1}: ${message}`);
state.status = "error" /* ERROR */;
state.timeouts.forEach((id) => clearTimeout(id));
state.timeouts = [];
const screenshotId = await captureScreenshot(
state.currentActionIndex,
window.location.href
);
state.currentActionIndex++;
window.parent.postMessage(
{
type: "staktrak-playwright-replay-error",
error: error instanceof Error ? error.message : "Unknown error",
actionIndex: state.currentActionIndex - 1,
action
error: message,
actionIndex: state.currentActionIndex,
action,
fatal: true,
screenshotId
},
getParentOrigin()
);
executeNextPlaywrightAction();
}
}
function pausePlaywrightReplay() {
Expand Down
35 changes: 34 additions & 1 deletion src/__tests__/unit/hooks/useStaktrakReplay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ describe('usePlaywrightReplay', () => {
});

describe('staktrak-playwright-replay-error', () => {
test('should accumulate errors without stopping replay', async () => {
test('should accumulate non-fatal errors without stopping replay', async () => {
const { result } = renderHook(() => usePlaywrightReplay(mockIframeRef));

act(() => {
Expand All @@ -644,6 +644,39 @@ describe('usePlaywrightReplay', () => {
});
});

test('should halt replay on a fatal error (issue #756)', async () => {
const { result } = renderHook(() => usePlaywrightReplay(mockIframeRef));

// Put the hook into a playing state first.
act(() => {
TestUtils.simulateMessageEvent({
type: 'staktrak-playwright-replay-progress',
current: 2,
total: 5,
action: 'click button',
});
});

act(() => {
TestUtils.simulateMessageEvent({
type: 'staktrak-playwright-replay-error',
error: 'Element not found',
actionIndex: 2,
action: 'click button',
fatal: true,
screenshotId: 'shot-123',
});
});

await waitFor(() => {
expect(result.current.replayErrors).toHaveLength(1);
expect(result.current.isPlaywrightReplaying).toBe(false);
expect(result.current.isPlaywrightPaused).toBe(false);
expect(result.current.playwrightStatus).toBe('error');
expect(result.current.currentAction).toBeNull();
});
});

test('should log error to console', async () => {
renderHook(() => usePlaywrightReplay(mockIframeRef));

Expand Down
22 changes: 22 additions & 0 deletions src/app/w/[slug]/task/[...taskParams]/artifacts/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,27 @@ export function BrowserArtifactPanel({
stopPlaywrightReplay,
replayScreenshots,
replayActions,
replayErrors = [],
} = usePlaywrightReplay(iframeRef, workspaceId, taskId, featureId, (message) => {
showActionToast("Screenshot Error", message);
});

// A step error halts the replay (issue #756), so surface which one broke.
const lastNotifiedErrorRef = useRef<string | null>(null);
useEffect(() => {
if (replayErrors.length === 0) {
lastNotifiedErrorRef.current = null;
return;
}
const latest = replayErrors[replayErrors.length - 1];
if (lastNotifiedErrorRef.current === latest.timestamp) return;
lastNotifiedErrorRef.current = latest.timestamp;
showActionToast(
`Replay stopped at action ${latest.actionIndex + 1}`,
latest.message,
);
}, [replayErrors, showActionToast]);

// Auto-show actions list when replay starts
useEffect(() => {
if (isPlaywrightReplaying && !showActions && (replayActions.length > 0 || capturedActions.length > 0)) {
Expand Down Expand Up @@ -452,6 +469,11 @@ export function BrowserArtifactPanel({
currentActionIndex={playwrightProgress.current - 1}
totalActions={playwrightProgress.total}
screenshots={replayScreenshots}
failedActionIndex={
replayErrors.length > 0
? replayErrors[replayErrors.length - 1].actionIndex
: -1
}
onReplayToggle={
capturedActions.length > 0 || isPlaywrightReplaying ? handleReplayToggle : undefined
}
Expand Down
33 changes: 24 additions & 9 deletions src/components/ActionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import { X, CheckCircle2, Loader2, Camera, Play, Square } from "lucide-react";
import { X, CheckCircle2, Loader2, Camera, Play, Square, XCircle } from "lucide-react";
import { useRef, useEffect, useState } from "react";
import { Screenshot } from "@/types/common";
import { ScreenshotModal } from "@/components/ScreenshotModal";
Expand Down Expand Up @@ -31,6 +31,8 @@ interface ActionsListProps {
screenshots?: Screenshot[];
title?: string;
onReplayToggle?: () => void;
/** Index of the action the replay failed on; -1 for none. */
failedActionIndex?: number;
}

// Helper function to extract the most descriptive element identifier
Expand Down Expand Up @@ -181,6 +183,7 @@ export function ActionsList({
screenshots = [],
title,
onReplayToggle,
failedActionIndex = -1,
}: ActionsListProps) {
const actionRefs = useRef<(HTMLDivElement | null)[]>([]);
const scrollContainerRef = useRef<HTMLDivElement>(null);
Expand All @@ -203,20 +206,29 @@ export function ActionsList({
}, [isReplaying, currentActionIndex]);

// Get action status based on replay progress
const getActionStatus = (index: number): "pending" | "active" | "completed" => {
const getActionStatus = (
index: number,
): "pending" | "active" | "completed" | "error" => {
// Checked before isReplaying so the failed step stays marked after the
// replay stops.
if (failedActionIndex >= 0 && index === failedActionIndex) return "error";
if (!isReplaying) return "pending";
if (index < currentActionIndex) return "completed";
if (index === currentActionIndex) return "active";
return "pending";
};

// Get status icon for action
const getStatusIcon = (status: "pending" | "active" | "completed") => {
const getStatusIcon = (
status: "pending" | "active" | "completed" | "error",
) => {
switch (status) {
case "completed":
return <CheckCircle2 className="h-3 w-3 text-green-500 flex-shrink-0" />;
case "active":
return <Loader2 className="h-3 w-3 text-blue-500 animate-spin flex-shrink-0" />;
case "error":
return <XCircle className="h-3 w-3 text-red-500 flex-shrink-0" />;
default:
return null;
}
Expand Down Expand Up @@ -281,6 +293,7 @@ export function ActionsList({
const status = getActionStatus(index);
const isActive = status === "active";
const isCompleted = status === "completed";
const isError = status === "error";
const screenshot = getScreenshotForAction(index);
const actionType = action.type;
// Only waitForURL actions get screenshots (goto is skipped)
Expand All @@ -296,16 +309,18 @@ export function ActionsList({
className={`flex items-center gap-2 rounded border-l-4 ${getActionBorderColor(
actionType,
)} p-1.5 transition-all duration-200 ${
isActive
? "bg-blue-100 dark:bg-blue-900/30 shadow-md ring-2 ring-blue-400 dark:ring-blue-600"
: isCompleted
? "bg-green-50 dark:bg-green-900/20 opacity-70"
: "bg-muted/50 hover:bg-muted"
isError
? "bg-red-100 dark:bg-red-900/30 shadow-md ring-2 ring-red-400 dark:ring-red-600"
: isActive
? "bg-blue-100 dark:bg-blue-900/30 shadow-md ring-2 ring-blue-400 dark:ring-blue-600"
: isCompleted
? "bg-green-50 dark:bg-green-900/20 opacity-70"
: "bg-muted/50 hover:bg-muted"
}`}
title={`${actionType}: ${action.url || action.locator?.text || action.locator?.primary || action.value || ""}`}
data-testid={`action-item-${index}`}
>
{isReplaying && getStatusIcon(status)}
{(isReplaying || isError) && getStatusIcon(status)}
{hasScreenshot && (
<button
onClick={() => setSelectedScreenshot(screenshot)}
Expand Down
14 changes: 13 additions & 1 deletion src/hooks/useStaktrakReplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,20 @@ export function usePlaywrightReplay(
},
]);

// Don't stop replay on error, just log it
console.warn("Playwright replay error:", errorMsg);

// The library already halted; mirror the `replay-stopped` cleanup.
if (data.fatal) {
setIsPlaywrightReplaying(false);
setIsPlaywrightPaused(false);
setPlaywrightStatus("error");
setCurrentAction(null);

const errContainer = document.querySelector(".iframe-container");
if (errContainer) {
errContainer.classList.remove("playwright-replaying");
}
}
break;

case "staktrak-playwright-replay-paused":
Expand Down
Loading