feat(FN-677): add browser verification workflow step template

- Add browser verification workflow step template to WORKFLOW_STEP_TEMPLATES
- Update NewTaskModal UI with browser verification checkbox
- Update InlineCreateCard UI with browser verification toggle
- Add tests for browser verification feature
- Create changeset for minor version bump
This commit is contained in:
gsxdsm
2026-04-02 10:20:22 -07:00
parent 814ce9e9d7
commit a075c549d3
6 changed files with 286 additions and 38 deletions

View File

@@ -0,0 +1,5 @@
---
"@gsxdsm/fusion": minor
---
Add Browser Verification workflow step template for agent-browser-based web app verification

View File

@@ -229,6 +229,42 @@ Output Requirements:
- If accessibility requirements are met: call task_done() with success status - If accessibility requirements are met: call task_done() with success status
- If issues found: describe each issue with specific file paths, WCAG guideline references, and remediation steps via task_log()`, - If issues found: describe each issue with specific file paths, WCAG guideline references, and remediation steps via task_log()`,
}, },
{
id: "browser-verification",
name: "Browser Verification",
description: "Verify web application functionality using browser automation",
category: "Quality",
icon: "globe",
prompt: `You are a browser verification specialist. Verify web application functionality after task implementation using the agent-browser CLI tool.
## Prerequisites
First, determine the URL to verify. Check the task PROMPT.md for any URLs mentioned, or look at the code changes to identify the local development server URL (typically http://localhost:3000, http://localhost:5173, http://localhost:8080, etc.).
## Verification Commands
Use these agent-browser commands for verification:
- \`agent-browser open <url>\` — Navigate to the page
- \`agent-browser snapshot -i\` — Get interactive elements with refs (@e1, @e2, etc.)
- \`agent-browser click @e1\` — Click an element
- \`agent-browser fill @e1 "text"\` — Fill an input field
- \`agent-browser get text @e1\` — Get element text content
- \`agent-browser screenshot\` — Capture screenshot to file
- \`agent-browser wait --load networkidle\` — Wait for page to fully load
## Verification Checklist
1. Page loads without JavaScript errors or blank screens
2. Navigation between pages/sections works
3. Forms accept input and submit correctly
4. Interactive elements (buttons, links) respond to clicks
5. Error states are handled gracefully
6. Screenshots capture expected content
## Output Requirements
- If verification succeeds: call task_done() with success status
- If verification fails: describe what failed and how it should behave via task_log()
- Include screenshots as evidence of verification results
Note: Refs (@e1, @e2) are invalidated after page navigation. Re-snapshot after clicking links or form submissions.`,
},
]; ];
export interface PrInfo { export interface PrInfo {

View File

@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import { Brain, Link, Lightbulb, ListTree, Zap, ChevronDown, ChevronUp } from "lucide-react"; import { Brain, Link, Lightbulb, ListTree, Zap, ChevronDown, ChevronUp, Globe } from "lucide-react";
import type { Task, TaskCreateInput, Settings } from "@fusion/core"; import type { Task, TaskCreateInput, Settings } from "@fusion/core";
import type { ToastType } from "../hooks/useToast"; import type { ToastType } from "../hooks/useToast";
import { fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings } from "../api"; import { fetchModels, uploadAttachment, fetchSettings, updateGlobalSettings } from "../api";
@@ -89,6 +89,7 @@ export function InlineCreateCard({
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [pendingImages, setPendingImages] = useState<PendingImage[]>([]); const [pendingImages, setPendingImages] = useState<PendingImage[]>([]);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [browserVerification, setBrowserVerification] = useState(false);
const justResetRef = useRef(false); const justResetRef = useRef(false);
const inputRef = useRef<HTMLTextAreaElement>(null); const inputRef = useRef<HTMLTextAreaElement>(null);
const cardRef = useRef<HTMLDivElement>(null); const cardRef = useRef<HTMLDivElement>(null);
@@ -284,6 +285,7 @@ export function InlineCreateCard({
modelId: hasExecutorOverride ? executorModelId : undefined, modelId: hasExecutorOverride ? executorModelId : undefined,
validatorModelProvider: hasValidatorOverride ? validatorProvider : undefined, validatorModelProvider: hasValidatorOverride ? validatorProvider : undefined,
validatorModelId: hasValidatorOverride ? validatorModelId : undefined, validatorModelId: hasValidatorOverride ? validatorModelId : undefined,
enabledWorkflowSteps: browserVerification ? ["browser-verification"] : undefined,
}); });
// Upload pending images as attachments // Upload pending images as attachments
@@ -318,6 +320,7 @@ export function InlineCreateCard({
setShowDeps(false); setShowDeps(false);
setShowModels(false); setShowModels(false);
setShowPresets(false); setShowPresets(false);
setBrowserVerification(false);
addToast(`Created ${task.id}`, "success"); addToast(`Created ${task.id}`, "success");
// Collapse and clear localStorage after successful task creation // Collapse and clear localStorage after successful task creation
@@ -345,6 +348,7 @@ export function InlineCreateCard({
onSubmit, onSubmit,
addToast, addToast,
selectedPresetId, selectedPresetId,
browserVerification,
]); ]);
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
@@ -463,6 +467,7 @@ export function InlineCreateCard({
setShowDeps(false); setShowDeps(false);
setShowModels(false); setShowModels(false);
setShowPresets(false); setShowPresets(false);
setBrowserVerification(false);
setIsExpanded(false); setIsExpanded(false);
}, [description, onPlanningMode, addToast]); }, [description, onPlanningMode, addToast]);
@@ -484,6 +489,7 @@ export function InlineCreateCard({
setShowDeps(false); setShowDeps(false);
setShowModels(false); setShowModels(false);
setShowPresets(false); setShowPresets(false);
setBrowserVerification(false);
setIsExpanded(false); setIsExpanded(false);
}, [description, onSubtaskBreakdown, addToast]); }, [description, onSubtaskBreakdown, addToast]);
@@ -775,6 +781,17 @@ export function InlineCreateCard({
<ListTree size={12} style={{ verticalAlign: "middle" }} /> <ListTree size={12} style={{ verticalAlign: "middle" }} />
Subtask Subtask
</button> </button>
<button
type="button"
className={`btn btn-sm ${browserVerification ? "btn-active" : ""}`}
onClick={() => setBrowserVerification((v) => !v)}
disabled={submitting}
data-testid="browser-verification-toggle"
title="Verify with agent-browser"
>
<Globe size={12} style={{ verticalAlign: "middle" }} />
Browser
</button>
</> </>
)} )}
</div> </div>

View File

@@ -5,7 +5,7 @@ import { uploadAttachment, fetchModels, fetchSettings, fetchWorkflowSteps, refin
import type { ModelInfo } from "../api"; import type { ModelInfo } from "../api";
import { applyPresetToSelection, getRecommendedPresetForSize } from "../utils/modelPresets"; import { applyPresetToSelection, getRecommendedPresetForSize } from "../utils/modelPresets";
import { CustomModelDropdown } from "./CustomModelDropdown"; import { CustomModelDropdown } from "./CustomModelDropdown";
import { Sparkles } from "lucide-react"; import { Sparkles, Globe } from "lucide-react";
const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]; const ALLOWED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
@@ -585,44 +585,71 @@ export function NewTaskModal({ isOpen, onClose, tasks, onCreateTask, addToast, o
</div> </div>
{/* Workflow Steps */} {/* Workflow Steps */}
{workflowSteps.length > 0 && ( <div className="form-group" data-testid="workflow-steps-section">
<div className="form-group" data-testid="workflow-steps-section"> <label>Workflow Steps</label>
<label>Workflow Steps</label> <small style={{ marginBottom: "8px", display: "block" }}>
<small style={{ marginBottom: "8px", display: "block" }}> Select steps to run after task implementation completes
Select steps to run after task implementation completes </small>
</small> <div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}> {workflowSteps.length > 0 && workflowSteps.map((step) => (
{workflowSteps.map((step) => ( <label
<label key={step.id}
key={step.id} className="checkbox-label"
className="checkbox-label" style={{ display: "flex", alignItems: "flex-start", gap: "8px" }}
style={{ display: "flex", alignItems: "flex-start", gap: "8px" }} data-testid={`workflow-step-checkbox-${step.id}`}
data-testid={`workflow-step-checkbox-${step.id}`} >
> <input
<input type="checkbox"
type="checkbox" checked={selectedWorkflowSteps.includes(step.id)}
checked={selectedWorkflowSteps.includes(step.id)} onChange={(e) => {
onChange={(e) => { setSelectedWorkflowSteps((prev) =>
setSelectedWorkflowSteps((prev) => e.target.checked
e.target.checked ? [...prev, step.id]
? [...prev, step.id] : prev.filter((id) => id !== step.id)
: prev.filter((id) => id !== step.id) );
); }}
}} disabled={isSubmitting}
disabled={isSubmitting} style={{ marginTop: "2px" }}
style={{ marginTop: "2px" }} />
/> <div>
<div> <span style={{ fontWeight: 500, fontSize: "13px" }}>{step.name}</span>
<span style={{ fontWeight: 500, fontSize: "13px" }}>{step.name}</span> <div style={{ fontSize: "12px", color: "var(--text-secondary)", marginTop: "2px" }}>
<div style={{ fontSize: "12px", color: "var(--text-secondary)", marginTop: "2px" }}> {step.description}
{step.description}
</div>
</div> </div>
</label> </div>
))} </label>
</div> ))}
<label
key="browser-verification"
className="checkbox-label"
style={{ display: "flex", alignItems: "flex-start", gap: "8px" }}
data-testid="browser-verification-checkbox"
>
<input
type="checkbox"
checked={selectedWorkflowSteps.includes("browser-verification")}
onChange={(e) => {
setSelectedWorkflowSteps((prev) =>
e.target.checked
? [...prev, "browser-verification"]
: prev.filter((id) => id !== "browser-verification")
);
}}
disabled={isSubmitting}
style={{ marginTop: "2px" }}
/>
<div>
<span style={{ fontWeight: 500, fontSize: "13px" }}>
<Globe size={14} style={{ verticalAlign: "middle", marginRight: "4px" }} />
Browser Verification
</span>
<div style={{ fontSize: "12px", color: "var(--text-secondary)", marginTop: "2px" }}>
Verify web application functionality using browser automation (agent-browser)
</div>
</div>
</label>
</div> </div>
)} </div>
<div className="form-group"> <div className="form-group">
<label>AI-assisted creation</label> <label>AI-assisted creation</label>

View File

@@ -19,6 +19,7 @@ vi.mock("lucide-react", () => ({
Zap: () => null, Zap: () => null,
ChevronDown: () => null, ChevronDown: () => null,
ChevronUp: () => null, ChevronUp: () => null,
Globe: () => null,
})); }));
// Mock the api module // Mock the api module
@@ -740,4 +741,89 @@ describe("InlineCreateCard button visibility when collapsed", () => {
// Footer should now be in the DOM // Footer should now be in the DOM
expect(document.getElementById("inline-create-controls")).toBeTruthy(); expect(document.getElementById("inline-create-controls")).toBeTruthy();
}); });
describe("browser verification", () => {
it("shows browser verification button when expanded", () => {
const { props } = renderCard();
// Button should not be visible when collapsed
expect(screen.queryByTestId("browser-verification-toggle")).toBeNull();
// Expand the card
expandCard();
// Button should now be visible
expect(screen.getByTestId("browser-verification-toggle")).toBeTruthy();
});
it("toggles browser verification when button is clicked", () => {
const { props } = renderCard();
expandCard();
const button = screen.getByTestId("browser-verification-toggle");
// Initially not active
expect(button).not.toHaveClass("btn-active");
// Click to enable
fireEvent.click(button);
expect(button).toHaveClass("btn-active");
// Click to disable
fireEvent.click(button);
expect(button).not.toHaveClass("btn-active");
});
it("includes browser-verification in enabledWorkflowSteps when submitting with browser verification enabled", async () => {
const mockOnSubmit = vi.fn().mockResolvedValue(createMockTask());
const { props } = renderCard([], { onSubmit: mockOnSubmit });
expandCard();
// Enable browser verification
fireEvent.click(screen.getByTestId("browser-verification-toggle"));
// Fill in description
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Test task with browser verification" } });
// Submit
fireEvent.click(screen.getByTestId("save-button"));
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith(
expect.objectContaining({
enabledWorkflowSteps: ["browser-verification"],
}),
);
});
});
it("resets browser verification state after successful submission", async () => {
const mockOnSubmit = vi.fn().mockResolvedValue(createMockTask({ id: "FN-042" }));
const { props } = renderCard([], { onSubmit: mockOnSubmit });
expandCard();
// Enable browser verification
fireEvent.click(screen.getByTestId("browser-verification-toggle"));
// Verify button is active
expect(screen.getByTestId("browser-verification-toggle")).toHaveClass("btn-active");
// Fill in description and submit
const textarea = screen.getByPlaceholderText("What needs to be done?");
fireEvent.change(textarea, { target: { value: "Task to complete" } });
fireEvent.click(screen.getByTestId("save-button"));
// Wait for submission to complete
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalled();
});
// Collapse and expand to reset
expandCard();
// Browser verification should be reset (button not active)
expect(screen.getByTestId("browser-verification-toggle")).not.toHaveClass("btn-active");
});
});
}); });

View File

@@ -3,6 +3,12 @@ import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { NewTaskModal } from "../NewTaskModal"; import { NewTaskModal } from "../NewTaskModal";
import type { Task, Column } from "@fusion/core"; import type { Task, Column } from "@fusion/core";
// Mock lucide-react
vi.mock("lucide-react", () => ({
Sparkles: () => null,
Globe: () => null,
}));
// Mock the api module // Mock the api module
vi.mock("../../api", () => ({ vi.mock("../../api", () => ({
uploadAttachment: vi.fn().mockResolvedValue({}), uploadAttachment: vi.fn().mockResolvedValue({}),
@@ -233,4 +239,75 @@ describe("NewTaskModal", () => {
const createButton = screen.getByRole("button", { name: "Create Task" }); const createButton = screen.getByRole("button", { name: "Create Task" });
expect(createButton).not.toBeDisabled(); expect(createButton).not.toBeDisabled();
}); });
// Browser verification tests
describe("browser verification", () => {
it("shows browser verification checkbox", () => {
renderNewTaskModal();
expect(screen.getByTestId("browser-verification-checkbox")).toBeTruthy();
expect(screen.getByText("Browser Verification")).toBeTruthy();
});
it("adds browser-verification to selected workflow steps when checkbox is checked", async () => {
const { props } = renderNewTaskModal();
const checkbox = screen.getByTestId("browser-verification-checkbox").querySelector('input[type="checkbox"]') as HTMLInputElement;
fireEvent.click(checkbox);
await waitFor(() => {
expect(checkbox.checked).toBe(true);
});
const descTextarea = screen.getByLabelText(/Description/i);
fireEvent.change(descTextarea, { target: { value: "Test task" } });
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
await waitFor(() => {
expect(props.onCreateTask).toHaveBeenCalledWith(
expect.objectContaining({
enabledWorkflowSteps: expect.arrayContaining(["browser-verification"]),
}),
);
});
});
it("removes browser-verification from selected workflow steps when checkbox is unchecked", async () => {
renderNewTaskModal();
const checkbox = screen.getByTestId("browser-verification-checkbox").querySelector('input[type="checkbox"]') as HTMLInputElement;
// Check then uncheck
fireEvent.click(checkbox);
await waitFor(() => {
expect(checkbox.checked).toBe(true);
});
fireEvent.click(checkbox);
await waitFor(() => {
expect(checkbox.checked).toBe(false);
});
});
it("includes browser-verification in enabledWorkflowSteps when submitting with checkbox checked", async () => {
const { props } = renderNewTaskModal();
const checkbox = screen.getByTestId("browser-verification-checkbox").querySelector('input[type="checkbox"]') as HTMLInputElement;
fireEvent.click(checkbox);
const descTextarea = screen.getByLabelText(/Description/i);
fireEvent.change(descTextarea, { target: { value: "Browser test task" } });
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
await waitFor(() => {
expect(props.onCreateTask).toHaveBeenCalledWith(
expect.objectContaining({
enabledWorkflowSteps: ["browser-verification"],
}),
);
});
});
});
}); });