fix: constrain dialog text inputs (#1437)
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// appsDir is the apps/ workspace root relative to the working directory (apps/backend/).
|
||||
@@ -185,6 +186,9 @@ func packageBundle(binDir, tarPath string) error {
|
||||
// Add Vite SPA assets. `kandev start` resolves these from /app/apps/web/dist
|
||||
// and passes KANDEV_WEB_DIST_DIR to the backend.
|
||||
webDistDir := filepath.Join(appsDir, "web", "dist")
|
||||
if err := validateViteWebDist(webDistDir); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := addDirToTar(tw, webDistDir, filepath.Join("app", "apps", "web", "dist")); err != nil {
|
||||
return fmt.Errorf("add web dist: %w", err)
|
||||
}
|
||||
@@ -201,6 +205,33 @@ func packageBundle(binDir, tarPath string) error {
|
||||
return gz.Close()
|
||||
}
|
||||
|
||||
func validateViteWebDist(webDistDir string) error {
|
||||
indexPath := filepath.Join(webDistDir, "index.html")
|
||||
indexHTML, err := os.ReadFile(indexPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read web dist index: %w", err)
|
||||
}
|
||||
if !viteIndexHasEntrypoint(string(indexHTML)) {
|
||||
return fmt.Errorf("web dist index %s is missing Vite module entrypoint", indexPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
scriptTagPattern = regexp.MustCompile(`(?is)<script\b[^>]*>`)
|
||||
moduleTypeAttrPattern = regexp.MustCompile(`(?is)\btype\s*=\s*["']module["']`)
|
||||
assetSrcAttrPattern = regexp.MustCompile(`(?is)\bsrc\s*=\s*["']/assets/`)
|
||||
)
|
||||
|
||||
func viteIndexHasEntrypoint(indexHTML string) bool {
|
||||
for _, tag := range scriptTagPattern.FindAllString(indexHTML, -1) {
|
||||
if moduleTypeAttrPattern.MatchString(tag) && assetSrcAttrPattern.MatchString(tag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func addFileToTar(tw *tar.Writer, src, dst string, mode fs.FileMode) error {
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestViteIndexHasEntrypoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
html string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "vite dist index",
|
||||
html: `<!doctype html><html><head><script type="module" crossorigin src="/assets/index-abc123.js"></script></head><body><div id="root"></div></body></html>`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "vite dist index with src before type and single quotes",
|
||||
html: `<!doctype html><html><head><script src='/assets/index-abc123.js' crossorigin type='module'></script></head><body><div id="root"></div></body></html>`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "vite dist index with spaced attribute assignment",
|
||||
html: `<!doctype html><html><head><script type = "module" crossorigin src = "/assets/index-abc123.js"></script></head><body><div id="root"></div></body></html>`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "fallback shell has no app entrypoint",
|
||||
html: `<!doctype html><html><head><title>Kandev</title></head><body><div id="root"></div></body></html>`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "source index still points at vite dev source",
|
||||
html: `<!doctype html><html><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>`,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "inline module script plus unrelated assets reference",
|
||||
html: `<!doctype html><html><head><link rel="stylesheet" href="/assets/index.css"></head><body><script type="module">console.log("x")</script></body></html>`,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got := viteIndexHasEntrypoint(tc.html); got != tc.want {
|
||||
t.Fatalf("viteIndexHasEntrypoint() = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -132,6 +132,9 @@ export KANDEV_DOCKER_ENABLED=false
|
||||
export KANDEV_LOG_LEVEL=info
|
||||
# Preview mode: only register the mock agent, suppress real agent discovery.
|
||||
export KANDEV_MOCK_AGENT=only
|
||||
# The preview service runs from /app, so the backend's relative dist probes do
|
||||
# not reach the packaged Vite build under /app/apps/web/dist.
|
||||
export KANDEV_WEB_DIST_DIR=/app/apps/web/dist
|
||||
|
||||
# Launch through the CLI so the backend runs under the restart supervisor.
|
||||
exec kandev start \
|
||||
|
||||
@@ -17,6 +17,9 @@ func TestBuildExtractScript(t *testing.T) {
|
||||
if !strings.Contains(script, "KANDEV_MOCK_AGENT=only") {
|
||||
t.Errorf("expected KANDEV_MOCK_AGENT=only in script")
|
||||
}
|
||||
if !strings.Contains(script, "KANDEV_WEB_DIST_DIR=/app/apps/web/dist") {
|
||||
t.Errorf("expected KANDEV_WEB_DIST_DIR to point at packaged Vite dist")
|
||||
}
|
||||
if !strings.Contains(script, "ln -sf /usr/local/lib/kandev-cli/bin/cli.js /usr/local/bin/kandev") {
|
||||
t.Errorf("expected kandev cli symlink in script")
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ var ErrSessionResetInProgress = errors.New("session reset in progress")
|
||||
var ErrSessionNotPromptable = errors.New("session not promptable")
|
||||
|
||||
const (
|
||||
agentPromptReadyTimeout = 10 * time.Second
|
||||
// Backend restart recovery can restore the session state before the ACP
|
||||
// stream is promptable again. Keep this above CI's slow-start tail so a
|
||||
// valid resume waits instead of surfacing "Failed to send message to agent".
|
||||
agentPromptReadyTimeout = 30 * time.Second
|
||||
agentPromptReadyInterval = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input bg-input/20 dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-md border px-2 py-2 text-sm transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"border-input bg-input/20 dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 resize-none rounded-md border px-2 py-2 text-sm transition-colors focus-visible:ring-[2px] aria-invalid:ring-[2px] md:text-xs/relaxed placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full min-w-0 max-w-full outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -270,7 +270,7 @@ export const InlineTaskName = memo(function InlineTaskName({
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="Task name"
|
||||
data-testid="task-title-input"
|
||||
className="w-full border border-input bg-input/20 dark:bg-input/30 text-sm font-medium rounded-md px-3 py-2 placeholder:text-muted-foreground/70 outline-none focus-visible:border-ring transition-colors"
|
||||
className="w-full min-w-0 max-w-full border border-input bg-input/20 dark:bg-input/30 text-sm font-medium rounded-md px-3 py-2 placeholder:text-muted-foreground/70 outline-none focus-visible:border-ring transition-colors"
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -733,13 +733,13 @@ export const TaskFormInputs = memo(function TaskFormInputs({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
className="relative min-w-0 max-w-full"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div
|
||||
className={`rounded-md border border-input bg-transparent ${contextItems.length > 0 ? "ring-0" : ""}`}
|
||||
className={`min-w-0 max-w-full rounded-md border border-input bg-transparent focus-within:ring-2 focus-within:ring-ring/30 ${contextItems.length > 0 ? "ring-0" : ""}`}
|
||||
>
|
||||
<ContextZone items={contextItems} />
|
||||
<Textarea
|
||||
@@ -756,7 +756,7 @@ export const TaskFormInputs = memo(function TaskFormInputs({
|
||||
onPaste={handlePaste}
|
||||
data-testid="task-description-input"
|
||||
rows={2}
|
||||
className={`border-0 focus-visible:ring-0 focus-visible:ring-offset-0 ${isSessionMode ? "min-h-[120px] max-h-[240px] resize-none overflow-auto text-[13px]" : "min-h-[96px] max-h-[240px] resize-y overflow-auto text-[13px]"}`}
|
||||
className={`min-w-0 max-w-full field-sizing-fixed wrap-anywhere border-0 focus-visible:ring-0 focus-visible:ring-offset-0 ${isSessionMode ? "min-h-[120px] max-h-[240px] resize-none overflow-auto text-[13px]" : "min-h-[96px] max-h-[240px] resize-y overflow-auto text-[13px]"}`}
|
||||
required={isSessionMode}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
@@ -493,7 +493,7 @@ export function TaskCreateDialog(props: TaskCreateDialogProps) {
|
||||
<Dialog open={props.open} onOpenChange={props.onOpenChange}>
|
||||
<DialogContent
|
||||
data-testid="create-task-dialog"
|
||||
className="w-full h-full max-w-full max-h-full rounded-none sm:w-[900px] sm:h-auto sm:max-w-none sm:max-h-[85vh] sm:rounded-lg flex flex-col"
|
||||
className="w-full h-full min-w-0 max-w-full max-h-full overflow-hidden rounded-none sm:w-[900px] sm:h-auto sm:max-w-none sm:max-h-[85vh] sm:rounded-lg flex flex-col"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogHeaderContent
|
||||
@@ -503,7 +503,10 @@ export function TaskCreateDialog(props: TaskCreateDialogProps) {
|
||||
initialTitle={props.initialValues?.title}
|
||||
/>
|
||||
</DialogHeader>
|
||||
<form onSubmit={guardedHandleSubmit} className="flex flex-col gap-4 overflow-hidden">
|
||||
<form
|
||||
onSubmit={guardedHandleSubmit}
|
||||
className="flex min-w-0 flex-col gap-4 overflow-hidden"
|
||||
>
|
||||
<DialogFormBody
|
||||
{...buildDialogFormBodyProps(setup, props)}
|
||||
onVoiceAutoSend={handleVoiceAutoSend}
|
||||
|
||||
@@ -12,9 +12,9 @@ export function ContextZone({ items, sessionId }: ContextZoneProps) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="overflow-y-auto max-h-28 border-b border-border/50 shrink-0">
|
||||
<div className="px-2 pt-2 pb-1 space-y-1.5">
|
||||
<div className="flex items-center gap-1 flex-wrap px-0 py-0.5">
|
||||
<div className="max-h-28 min-w-0 shrink-0 overflow-y-auto border-b border-border/50">
|
||||
<div className="min-w-0 space-y-1.5 px-2 pt-2 pb-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-1 px-0 py-0.5">
|
||||
{items.map((item) => (
|
||||
<ContextItemRenderer key={item.id} item={item} sessionId={sessionId} />
|
||||
))}
|
||||
|
||||
@@ -402,7 +402,7 @@ function NewSessionForm({
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<form onSubmit={handleSubmit} className="min-w-0 space-y-4">
|
||||
<SessionFormHeader
|
||||
executorLabel={executorLabel}
|
||||
worktreeBranch={worktreeBranch}
|
||||
@@ -524,9 +524,9 @@ export function NewSessionDialog({
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[520px]">
|
||||
<DialogContent className="min-w-0 overflow-hidden sm:max-w-[520px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm font-medium">
|
||||
<DialogTitle className="min-w-0 wrap-break-word pr-6 text-sm font-medium">
|
||||
{handoffLabel ? (
|
||||
<>
|
||||
Hand off to <span className="text-foreground">{handoffLabel}</span>
|
||||
|
||||
@@ -52,13 +52,18 @@ export function SessionPromptField({
|
||||
onFileInputChange,
|
||||
}: SessionPromptFieldProps) {
|
||||
return (
|
||||
<div className="relative" onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}>
|
||||
<div className="rounded-md border border-input bg-transparent">
|
||||
<div
|
||||
className="relative min-w-0 max-w-full"
|
||||
onDragOver={onDragOver}
|
||||
onDragLeave={onDragLeave}
|
||||
onDrop={onDrop}
|
||||
>
|
||||
<div className="min-w-0 max-w-full rounded-md border border-input bg-transparent focus-within:ring-2 focus-within:ring-ring/30">
|
||||
<ContextZone items={contextItems} />
|
||||
<Textarea
|
||||
ref={promptRef}
|
||||
placeholder="What should the agent work on?"
|
||||
className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 min-h-[120px] max-h-[240px] resize-none overflow-auto text-[13px]"
|
||||
className="min-w-0 max-w-full field-sizing-fixed wrap-anywhere border-0 focus-visible:ring-0 focus-visible:ring-offset-0 min-h-[120px] max-h-[240px] resize-none overflow-auto text-[13px]"
|
||||
autoFocus
|
||||
disabled={isBusy}
|
||||
onInput={onPromptInput}
|
||||
|
||||
@@ -360,10 +360,10 @@ export function NewSubtaskDialog({
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
data-testid="new-subtask-dialog"
|
||||
className="w-full h-full max-w-full max-h-full rounded-none sm:w-[800px] sm:h-auto sm:max-w-none sm:max-h-[85vh] sm:rounded-lg flex flex-col"
|
||||
className="w-full h-full min-w-0 max-w-full max-h-full overflow-hidden rounded-none sm:w-[800px] sm:h-auto sm:max-w-none sm:max-h-[85vh] sm:rounded-lg flex flex-col"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm font-medium">
|
||||
<DialogTitle className="min-w-0 wrap-break-word pr-6 text-sm font-medium">
|
||||
New subtask for <span className="text-foreground">{parentTaskTitle}</span>
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -128,17 +128,17 @@ export function PromptZone({
|
||||
const inputDisabled = isCreating || isSummarizing;
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
className="relative min-w-0 max-w-full"
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<div className="rounded-md border border-input bg-transparent">
|
||||
<div className="min-w-0 max-w-full rounded-md border border-input bg-transparent focus-within:ring-2 focus-within:ring-ring/30">
|
||||
<ContextZone items={contextItems} />
|
||||
<Textarea
|
||||
ref={promptRef}
|
||||
placeholder="What should the agent work on?"
|
||||
className="border-0 focus-visible:ring-0 focus-visible:ring-offset-0 min-h-[120px] max-h-[240px] resize-none overflow-auto text-[13px]"
|
||||
className="min-w-0 max-w-full field-sizing-fixed wrap-anywhere border-0 focus-visible:ring-0 focus-visible:ring-offset-0 min-h-[120px] max-h-[240px] resize-none overflow-auto text-[13px]"
|
||||
autoFocus
|
||||
disabled={inputDisabled}
|
||||
data-testid="subtask-prompt-input"
|
||||
@@ -400,7 +400,7 @@ export function SubtaskFormBody({
|
||||
const showWorktreeBadge = shouldShowWorktreeBadge(fs, worktreeBranch, parentRepositoryId);
|
||||
const inheritParent = workspaceMode === "inherit_parent";
|
||||
return (
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<form onSubmit={onSubmit} className="min-w-0 space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="subtask-title-input" className="text-xs font-medium text-muted-foreground">
|
||||
Title
|
||||
@@ -410,7 +410,7 @@ export function SubtaskFormBody({
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Subtask title"
|
||||
className="text-sm"
|
||||
className="min-w-0 max-w-full text-sm"
|
||||
data-testid="subtask-title-input"
|
||||
disabled={isCreating}
|
||||
/>
|
||||
|
||||
@@ -33,19 +33,21 @@ export function EnvironmentBadges({
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
{executorLabel && (
|
||||
<Badge variant="secondary" className="text-xs font-normal">
|
||||
{executorLabel}
|
||||
</Badge>
|
||||
)}
|
||||
{worktreeBranch && (
|
||||
<Badge variant="outline" className="text-xs font-normal gap-1">
|
||||
<Badge variant="outline" className="min-w-0 max-w-full gap-1 text-xs font-normal">
|
||||
<IconGitBranch className="h-3 w-3" />
|
||||
{worktreeBranch}
|
||||
<span className="min-w-0 truncate">{worktreeBranch}</span>
|
||||
</Badge>
|
||||
)}
|
||||
<span>{description ?? "Same environment as current session"}</span>
|
||||
<span className="min-w-0 break-words">
|
||||
{description ?? "Same environment as current session"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -80,9 +82,9 @@ export function ContextSelect({
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<label className="text-xs font-medium text-muted-foreground">Context</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<Select value={value} onValueChange={onValueChange} disabled={isSummarizing}>
|
||||
<SelectTrigger className="w-full text-xs">
|
||||
<SelectTrigger className="w-full min-w-0 text-xs">
|
||||
<SelectValue>{isSummarizing ? "Summarizing..." : displayLabel}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
||||
@@ -47,9 +47,10 @@ async function getSidebarWidth(sidebar: Locator): Promise<number> {
|
||||
return Math.round(box.width);
|
||||
}
|
||||
|
||||
// boundingBox returns floats and sub-pixel rounding can shift the measured
|
||||
// width by a pixel across renderers, so compare with a small tolerance.
|
||||
function expectWidthNear(actual: number, expected: number, tolerance = 1) {
|
||||
// boundingBox returns rendered border-box floats; flex layout, borders, and
|
||||
// sub-pixel rounding can shift the measured width by a couple of pixels across
|
||||
// CI/browser environments, so compare with a small tolerance.
|
||||
function expectWidthNear(actual: number, expected: number, tolerance = 2) {
|
||||
expect(actual).toBeGreaterThanOrEqual(expected - tolerance);
|
||||
expect(actual).toBeLessThanOrEqual(expected + tolerance);
|
||||
}
|
||||
@@ -66,7 +67,7 @@ async function dragHandle(testPage: Page, handle: Locator, deltaX: number) {
|
||||
}
|
||||
|
||||
test.describe("Review dialog sidebar resize", () => {
|
||||
test.describe.configure({ retries: 2, timeout: 120_000 });
|
||||
test.describe.configure({ timeout: 120_000 });
|
||||
|
||||
test("dragging the handle resizes the sidebar and persists to sessionStorage", async ({
|
||||
testPage,
|
||||
|
||||
@@ -19,9 +19,6 @@ async function openTaskSession(page: Page, title: string): Promise<SessionPage>
|
||||
}
|
||||
|
||||
test.describe("Executor not found after backend restart", () => {
|
||||
// Backend restart tests can be flaky
|
||||
test.describe.configure({ retries: 1 });
|
||||
|
||||
test("agent starts successfully when sending new prompt after backend restart", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
@@ -68,6 +65,6 @@ test.describe("Executor not found after backend restart", () => {
|
||||
await session.sendMessage("/e2e:simple-message");
|
||||
|
||||
// 7. The agent should respond successfully (not fail with executor not found)
|
||||
await session.expectChatResponseVisible("simple mock response", 1, { timeout: 30_000 });
|
||||
await session.expectChatResponseVisible("simple mock response", 1, { timeout: 60_000 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test, expect } from "../../fixtures/test-base";
|
||||
import { assertNoDescendantOverflowsRight } from "../../helpers/layout-assertions";
|
||||
import { useRegularMode } from "../../helpers/regular-mode";
|
||||
import { KanbanPage } from "../../pages/kanban-page";
|
||||
import { SessionPage } from "../../pages/session-page";
|
||||
|
||||
// Exercises the regular task/subtask dialogs, so run with the office feature disabled.
|
||||
useRegularMode();
|
||||
|
||||
const LONG_UNBROKEN_TEXT = `review-${"x".repeat(240)}-https://github.com/example/repo/commit/${"a".repeat(80)}`;
|
||||
|
||||
test.describe("Dialog long text layout", () => {
|
||||
test("agent, task, and subtask dialogs stay within their modal bounds", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
}) => {
|
||||
const kanban = new KanbanPage(testPage);
|
||||
await kanban.goto();
|
||||
|
||||
await kanban.createTaskButton.first().click();
|
||||
const createDialog = testPage.getByTestId("create-task-dialog");
|
||||
await expect(createDialog).toBeVisible();
|
||||
await testPage.getByTestId("task-title-input").fill(LONG_UNBROKEN_TEXT);
|
||||
await testPage.getByTestId("task-description-input").fill(LONG_UNBROKEN_TEXT);
|
||||
await assertNoDescendantOverflowsRight(createDialog, "create task dialog");
|
||||
await testPage.keyboard.press("Escape");
|
||||
await expect(createDialog).not.toBeVisible();
|
||||
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Long Text Dialog Parent",
|
||||
seedData.agentProfileId,
|
||||
{
|
||||
description: "/e2e:simple-message",
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
|
||||
await session.openNewSessionDialog();
|
||||
const newSessionDialog = session.newSessionDialog();
|
||||
await expect(newSessionDialog).toBeVisible();
|
||||
await session.newSessionPromptInput().fill(LONG_UNBROKEN_TEXT);
|
||||
await assertNoDescendantOverflowsRight(newSessionDialog, "new agent dialog");
|
||||
await testPage.keyboard.press("Escape");
|
||||
await expect(newSessionDialog).not.toBeVisible();
|
||||
|
||||
await testPage.getByTestId("sidebar-new-subtask").click();
|
||||
const subtaskDialog = testPage.getByTestId("new-subtask-dialog");
|
||||
await expect(subtaskDialog).toBeVisible();
|
||||
await testPage.getByTestId("subtask-title-input").fill(LONG_UNBROKEN_TEXT);
|
||||
await testPage.getByTestId("subtask-prompt-input").fill(LONG_UNBROKEN_TEXT);
|
||||
await assertNoDescendantOverflowsRight(subtaskDialog, "new subtask dialog");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user