mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:10:45 +00:00
test: merge more ui render hotspots
This commit is contained in:
@@ -39,72 +39,6 @@ function expectConfirmedGatewayChange(app: ReturnType<typeof mountApp>) {
|
||||
}
|
||||
|
||||
describe("control UI routing", () => {
|
||||
it("renders the dreaming view on the /dreaming route", async () => {
|
||||
const app = mountApp("/dreaming");
|
||||
app.dreamingStatus = {
|
||||
enabled: true,
|
||||
timezone: "Europe/Madrid",
|
||||
verboseLogging: false,
|
||||
storageMode: "inline",
|
||||
separateReports: false,
|
||||
shortTermCount: 2,
|
||||
recallSignalCount: 1,
|
||||
dailySignalCount: 1,
|
||||
groundedSignalCount: 0,
|
||||
totalSignalCount: 2,
|
||||
phaseSignalCount: 0,
|
||||
lightPhaseHitCount: 0,
|
||||
remPhaseHitCount: 0,
|
||||
promotedTotal: 1,
|
||||
promotedToday: 1,
|
||||
shortTermEntries: [],
|
||||
signalEntries: [],
|
||||
promotedEntries: [],
|
||||
phases: {
|
||||
light: { enabled: true, cron: "", managedCronPresent: false, lookbackDays: 7, limit: 20 },
|
||||
deep: {
|
||||
enabled: true,
|
||||
cron: "",
|
||||
managedCronPresent: false,
|
||||
limit: 20,
|
||||
minScore: 0.75,
|
||||
minRecallCount: 3,
|
||||
minUniqueQueries: 2,
|
||||
recencyHalfLifeDays: 7,
|
||||
},
|
||||
rem: {
|
||||
enabled: true,
|
||||
cron: "",
|
||||
managedCronPresent: false,
|
||||
lookbackDays: 7,
|
||||
limit: 20,
|
||||
minPatternStrength: 0.6,
|
||||
},
|
||||
},
|
||||
};
|
||||
app.dreamDiaryPath = "DREAMS.md";
|
||||
app.dreamDiaryContent = [
|
||||
"# Dream Diary",
|
||||
"",
|
||||
"<!-- openclaw:dreaming:diary:start -->",
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"*January 1, 2026*",
|
||||
"",
|
||||
"What Happened",
|
||||
"1. Stable operator rule surfaced.",
|
||||
"",
|
||||
"<!-- openclaw:dreaming:diary:end -->",
|
||||
].join("\n");
|
||||
app.requestUpdate();
|
||||
await app.updateComplete;
|
||||
|
||||
expect(app.tab).toBe("dreams");
|
||||
expect(app.querySelector(".dreams__tab")).not.toBeNull();
|
||||
expect(app.querySelector(".dreams__lobster")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders responsive navigation shell, drawer, and collapsed states", async () => {
|
||||
const app = mountApp("/chat");
|
||||
await app.updateComplete;
|
||||
@@ -127,6 +61,19 @@ describe("control UI routing", () => {
|
||||
expect(app.querySelector(".sidebar-brand__logo")).not.toBeNull();
|
||||
expect(app.querySelector(".sidebar-brand__copy")).not.toBeNull();
|
||||
|
||||
app.hello = {
|
||||
ok: true,
|
||||
server: { version: "1.2.3" },
|
||||
} as never;
|
||||
app.requestUpdate();
|
||||
await app.updateComplete;
|
||||
|
||||
const version = app.querySelector<HTMLElement>(".sidebar-version");
|
||||
const statusDot = app.querySelector<HTMLElement>(".sidebar-version__status");
|
||||
expect(version).not.toBeNull();
|
||||
expect(statusDot).not.toBeNull();
|
||||
expect(statusDot?.getAttribute("aria-label")).toContain("Online");
|
||||
|
||||
app.applySettings({ ...app.settings, navWidth: 360 });
|
||||
await app.updateComplete;
|
||||
|
||||
@@ -274,24 +221,6 @@ describe("control UI routing", () => {
|
||||
expect(shell?.classList.contains("shell--chat-focus")).toBe(true);
|
||||
});
|
||||
|
||||
it("shows one online status dot next to the sidebar version", async () => {
|
||||
const app = mountApp("/chat");
|
||||
await app.updateComplete;
|
||||
|
||||
app.hello = {
|
||||
ok: true,
|
||||
server: { version: "1.2.3" },
|
||||
} as never;
|
||||
app.requestUpdate();
|
||||
await app.updateComplete;
|
||||
|
||||
const version = app.querySelector<HTMLElement>(".sidebar-version");
|
||||
const statusDot = app.querySelector<HTMLElement>(".sidebar-version__status");
|
||||
expect(version).not.toBeNull();
|
||||
expect(statusDot).not.toBeNull();
|
||||
expect(statusDot?.getAttribute("aria-label")).toContain("Online");
|
||||
});
|
||||
|
||||
it("auto-scrolls chat history to the latest message", async () => {
|
||||
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
|
||||
queueMicrotask(() => callback(performance.now()));
|
||||
|
||||
@@ -144,15 +144,12 @@ describe("renderAgents", () => {
|
||||
);
|
||||
await Promise.resolve();
|
||||
|
||||
const skillsTab = Array.from(container.querySelectorAll<HTMLButtonElement>(".agent-tab")).find(
|
||||
let skillsTab = Array.from(container.querySelectorAll<HTMLButtonElement>(".agent-tab")).find(
|
||||
(button) => button.textContent?.includes("Skills"),
|
||||
);
|
||||
|
||||
expect(skillsTab?.textContent?.trim()).toBe("Skills");
|
||||
});
|
||||
|
||||
it("shows the selected agent's skills count when the report matches", async () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderAgents(
|
||||
createProps({
|
||||
@@ -173,7 +170,7 @@ describe("renderAgents", () => {
|
||||
);
|
||||
await Promise.resolve();
|
||||
|
||||
const skillsTab = Array.from(container.querySelectorAll<HTMLButtonElement>(".agent-tab")).find(
|
||||
skillsTab = Array.from(container.querySelectorAll<HTMLButtonElement>(".agent-tab")).find(
|
||||
(button) => button.textContent?.includes("Skills"),
|
||||
);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getSafeLocalStorage } from "../../local-storage.ts";
|
||||
import { resetAssistantAttachmentAvailabilityCacheForTest } from "../chat/grouped-render.ts";
|
||||
import { normalizeMessage } from "../chat/message-normalizer.ts";
|
||||
import type { SessionsListResult } from "../types.ts";
|
||||
import { renderChat, type ChatProps } from "./chat.ts";
|
||||
import { getContextNoticeViewModel, renderChat, type ChatProps } from "./chat.ts";
|
||||
|
||||
function createSessions(): SessionsListResult {
|
||||
return {
|
||||
@@ -162,16 +162,19 @@ describe("chat view", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
renderWithSession({
|
||||
key: "main",
|
||||
kind: "direct",
|
||||
updatedAt: null,
|
||||
inputTokens: 757_300,
|
||||
totalTokens: 46_000,
|
||||
contextTokens: 200_000,
|
||||
});
|
||||
expect(container.textContent).not.toContain("context used");
|
||||
expect(container.textContent).not.toContain("757.3k / 200k");
|
||||
expect(
|
||||
getContextNoticeViewModel(
|
||||
{
|
||||
key: "main",
|
||||
kind: "direct",
|
||||
updatedAt: null,
|
||||
inputTokens: 757_300,
|
||||
totalTokens: 46_000,
|
||||
contextTokens: 200_000,
|
||||
},
|
||||
200_000,
|
||||
),
|
||||
).toBeNull();
|
||||
|
||||
renderWithSession({
|
||||
key: "main",
|
||||
@@ -201,25 +204,31 @@ describe("chat view", () => {
|
||||
document.documentElement.style.removeProperty("--warn");
|
||||
document.documentElement.style.removeProperty("--danger");
|
||||
|
||||
renderWithSession({
|
||||
key: "main",
|
||||
kind: "direct",
|
||||
updatedAt: null,
|
||||
inputTokens: 500_000,
|
||||
contextTokens: 200_000,
|
||||
});
|
||||
expect(container.textContent).not.toContain("context used");
|
||||
|
||||
renderWithSession({
|
||||
key: "main",
|
||||
kind: "direct",
|
||||
updatedAt: null,
|
||||
totalTokens: 190_000,
|
||||
totalTokensFresh: false,
|
||||
contextTokens: 200_000,
|
||||
});
|
||||
expect(container.textContent).not.toContain("context used");
|
||||
expect(container.textContent).not.toContain("190k / 200k");
|
||||
expect(
|
||||
getContextNoticeViewModel(
|
||||
{
|
||||
key: "main",
|
||||
kind: "direct",
|
||||
updatedAt: null,
|
||||
inputTokens: 500_000,
|
||||
contextTokens: 200_000,
|
||||
},
|
||||
200_000,
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
getContextNoticeViewModel(
|
||||
{
|
||||
key: "main",
|
||||
kind: "direct",
|
||||
updatedAt: null,
|
||||
totalTokens: 190_000,
|
||||
totalTokensFresh: false,
|
||||
contextTokens: 200_000,
|
||||
},
|
||||
200_000,
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("uses the assistant avatar URL or bundled logo fallbacks", () => {
|
||||
|
||||
@@ -529,21 +529,26 @@ function getThemeNoticeColors() {
|
||||
return cachedThemeNoticeColors;
|
||||
}
|
||||
|
||||
function renderContextNotice(
|
||||
export function getContextNoticeViewModel(
|
||||
session: GatewaySessionRow | undefined,
|
||||
defaultContextTokens: number | null,
|
||||
) {
|
||||
): {
|
||||
pct: number;
|
||||
detail: string;
|
||||
color: string;
|
||||
bg: string;
|
||||
} | null {
|
||||
if (session?.totalTokensFresh === false) {
|
||||
return nothing;
|
||||
return null;
|
||||
}
|
||||
const used = session?.totalTokens ?? 0;
|
||||
const limit = session?.contextTokens ?? defaultContextTokens ?? 0;
|
||||
if (!used || !limit) {
|
||||
return nothing;
|
||||
return null;
|
||||
}
|
||||
const ratio = used / limit;
|
||||
if (ratio < 0.85) {
|
||||
return nothing;
|
||||
return null;
|
||||
}
|
||||
const pct = Math.min(Math.round(ratio * 100), 100);
|
||||
// Read theme semantic tokens so color tracks the active theme (Dash, dark, light …)
|
||||
@@ -558,8 +563,28 @@ function renderContextNotice(
|
||||
const color = `rgb(${r}, ${g}, ${b})`;
|
||||
const bgOpacity = 0.08 + 0.08 * t;
|
||||
const bg = `rgba(${r}, ${g}, ${b}, ${bgOpacity})`;
|
||||
return {
|
||||
pct,
|
||||
detail: `${formatTokensCompact(used)} / ${formatTokensCompact(limit)}`,
|
||||
color,
|
||||
bg,
|
||||
};
|
||||
}
|
||||
|
||||
function renderContextNotice(
|
||||
session: GatewaySessionRow | undefined,
|
||||
defaultContextTokens: number | null,
|
||||
) {
|
||||
const model = getContextNoticeViewModel(session, defaultContextTokens);
|
||||
if (!model) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div class="context-notice" role="status" style="--ctx-color:${color};--ctx-bg:${bg}">
|
||||
<div
|
||||
class="context-notice"
|
||||
role="status"
|
||||
style="--ctx-color:${model.color};--ctx-bg:${model.bg}"
|
||||
>
|
||||
<svg
|
||||
class="context-notice__icon"
|
||||
width="16"
|
||||
@@ -575,10 +600,8 @@ function renderContextNotice(
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
<span>${pct}% context used</span>
|
||||
<span class="context-notice__detail"
|
||||
>${formatTokensCompact(used)} / ${formatTokensCompact(limit)}</span
|
||||
>
|
||||
<span>${model.pct}% context used</span>
|
||||
<span class="context-notice__detail">${model.detail}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -361,43 +361,46 @@ describe("config view", () => {
|
||||
expect(onRawChange).toHaveBeenCalledWith(textarea.value);
|
||||
});
|
||||
|
||||
it("renders structured SecretRef values as read-only text inputs without stringifying", () => {
|
||||
it("renders structured SecretRef values without stringifying", () => {
|
||||
const onFormPatch = vi.fn();
|
||||
const { container } = renderConfigView({
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
channels: {
|
||||
type: "object",
|
||||
properties: {
|
||||
discord: {
|
||||
type: "object",
|
||||
properties: {
|
||||
token: { type: "string" },
|
||||
},
|
||||
const secretRefSchema = {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
channels: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
discord: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
token: { type: "string" as const },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const secretRefValue = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "__OPENCLAW_REDACTED__" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const secretRefOriginalValue = {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
},
|
||||
},
|
||||
};
|
||||
const { container } = renderConfigView({
|
||||
schema: secretRefSchema,
|
||||
uiHints: {
|
||||
"channels.discord.token": { sensitive: true },
|
||||
},
|
||||
formMode: "form",
|
||||
formValue: {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "__OPENCLAW_REDACTED__" },
|
||||
},
|
||||
},
|
||||
},
|
||||
originalValue: {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
formValue: secretRefValue,
|
||||
originalValue: secretRefOriginalValue,
|
||||
onFormPatch,
|
||||
});
|
||||
|
||||
@@ -415,50 +418,27 @@ describe("config view", () => {
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
expect(onFormPatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses a file-edit placeholder for structured SecretRefs when raw mode is unavailable", () => {
|
||||
const { container } = renderConfigView({
|
||||
rawAvailable: false,
|
||||
formMode: "raw",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
channels: {
|
||||
type: "object",
|
||||
properties: {
|
||||
discord: {
|
||||
type: "object",
|
||||
properties: {
|
||||
token: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
render(
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
rawAvailable: false,
|
||||
formMode: "raw",
|
||||
schema: secretRefSchema,
|
||||
uiHints: {
|
||||
"channels.discord.token": { sensitive: true },
|
||||
},
|
||||
},
|
||||
uiHints: {
|
||||
"channels.discord.token": { sensitive: true },
|
||||
},
|
||||
formValue: {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "__OPENCLAW_REDACTED__" },
|
||||
},
|
||||
},
|
||||
},
|
||||
originalValue: {
|
||||
channels: {
|
||||
discord: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_BOT_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
formValue: secretRefValue,
|
||||
originalValue: secretRefOriginalValue,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const input = container.querySelector<HTMLInputElement>(".cfg-input");
|
||||
expect(input).not.toBeNull();
|
||||
expect(input?.placeholder).toBe("Structured value (SecretRef) - edit the config file directly");
|
||||
const rawUnavailableInput = container.querySelector<HTMLInputElement>(".cfg-input");
|
||||
expect(rawUnavailableInput).not.toBeNull();
|
||||
expect(rawUnavailableInput?.placeholder).toBe(
|
||||
"Structured value (SecretRef) - edit the config file directly",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps malformed non-SecretRef object values editable when raw mode is unavailable", () => {
|
||||
|
||||
@@ -84,10 +84,30 @@ function getButtonByText(container: Element, text: string) {
|
||||
}
|
||||
|
||||
describe("cron view", () => {
|
||||
it("shows all-job history mode and toggles the run status filter", () => {
|
||||
it("shows all-job history mode and wires run/job filters", () => {
|
||||
const container = document.createElement("div");
|
||||
const onRunsFiltersChange = vi.fn();
|
||||
render(renderCron(createProps({ onRunsFiltersChange })), container);
|
||||
const onJobsFiltersChange = vi.fn();
|
||||
const onJobsFiltersReset = vi.fn();
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
onRunsFiltersChange,
|
||||
onJobsFiltersChange,
|
||||
runsScope: "all",
|
||||
runs: [
|
||||
{
|
||||
ts: Date.now(),
|
||||
jobId: "job-1",
|
||||
status: "ok",
|
||||
summary: "done",
|
||||
nextRunAtMs: Date.now() - 13 * 60_000,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("Latest runs across all jobs.");
|
||||
expect(container.textContent).toContain("Status");
|
||||
@@ -107,118 +127,9 @@ describe("cron view", () => {
|
||||
statusOk.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
|
||||
expect(onRunsFiltersChange).toHaveBeenCalledWith({ cronRunsStatuses: ["ok"] });
|
||||
});
|
||||
|
||||
it("marks the selected job and routes row/history clicks to run history", () => {
|
||||
const container = document.createElement("div");
|
||||
const onLoadRuns = vi.fn();
|
||||
const job = createJob("job-1");
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
jobs: [job],
|
||||
runsJobId: "job-1",
|
||||
runsScope: "job",
|
||||
onLoadRuns,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const selected = container.querySelector(".list-item-selected");
|
||||
expect(selected).not.toBeNull();
|
||||
|
||||
const row = container.querySelector(".list-item-clickable");
|
||||
expect(row).not.toBeNull();
|
||||
row?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onLoadRuns).toHaveBeenCalledWith("job-1");
|
||||
|
||||
const historyButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "History",
|
||||
);
|
||||
expect(historyButton).not.toBeUndefined();
|
||||
historyButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(onLoadRuns).toHaveBeenCalledTimes(2);
|
||||
expect(onLoadRuns).toHaveBeenNthCalledWith(1, "job-1");
|
||||
expect(onLoadRuns).toHaveBeenNthCalledWith(2, "job-1");
|
||||
});
|
||||
|
||||
it("shows selected job run history sorted newest first with chat links", () => {
|
||||
const container = document.createElement("div");
|
||||
const job = createJob("job-1");
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
basePath: "/ui",
|
||||
jobs: [job],
|
||||
runsJobId: "job-1",
|
||||
runsScope: "job",
|
||||
runs: [
|
||||
{ ts: 1, jobId: "job-1", status: "ok", summary: "older run" },
|
||||
{
|
||||
ts: 2,
|
||||
jobId: "job-1",
|
||||
status: "ok",
|
||||
summary: "newer run",
|
||||
sessionKey: "agent:main:cron:job-1:run:abc",
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const link = container.querySelector("a.session-link");
|
||||
expect(link).not.toBeNull();
|
||||
expect(link?.getAttribute("href")).toContain(
|
||||
"/ui/chat?session=agent%3Amain%3Acron%3Ajob-1%3Arun%3Aabc",
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("Latest runs for Daily ping.");
|
||||
|
||||
const cards = Array.from(container.querySelectorAll(".card"));
|
||||
const runHistoryCard = cards.find(
|
||||
(card) => card.querySelector(".card-title")?.textContent?.trim() === "Run history",
|
||||
);
|
||||
expect(runHistoryCard).not.toBeUndefined();
|
||||
|
||||
const summaries = Array.from(
|
||||
runHistoryCard?.querySelectorAll(".list-item .list-sub") ?? [],
|
||||
).map((el) => (el.textContent ?? "").trim());
|
||||
expect(summaries[0]).toBe("newer run");
|
||||
expect(summaries[1]).toBe("older run");
|
||||
});
|
||||
|
||||
it("labels past nextRunAtMs as due instead of next", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
runsScope: "all",
|
||||
runs: [
|
||||
{
|
||||
ts: Date.now(),
|
||||
jobId: "job-1",
|
||||
status: "ok",
|
||||
summary: "done",
|
||||
nextRunAtMs: Date.now() - 13 * 60_000,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("Due");
|
||||
expect(container.textContent).not.toContain("Next 13");
|
||||
});
|
||||
|
||||
it("wires jobs filter changes and reset", () => {
|
||||
const container = document.createElement("div");
|
||||
const onJobsFiltersChange = vi.fn();
|
||||
const onJobsFiltersReset = vi.fn();
|
||||
render(renderCron(createProps({ onJobsFiltersChange })), container);
|
||||
|
||||
const scheduleSelect = container.querySelector(
|
||||
'select[data-test-id="cron-jobs-schedule-filter"]',
|
||||
@@ -261,6 +172,72 @@ describe("cron view", () => {
|
||||
expect(onJobsFiltersReset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("marks the selected job, routes history clicks, and sorts runs newest first", () => {
|
||||
const container = document.createElement("div");
|
||||
const onLoadRuns = vi.fn();
|
||||
const job = createJob("job-1");
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
basePath: "/ui",
|
||||
jobs: [job],
|
||||
runsJobId: "job-1",
|
||||
runsScope: "job",
|
||||
runs: [
|
||||
{ ts: 1, jobId: "job-1", status: "ok", summary: "older run" },
|
||||
{
|
||||
ts: 2,
|
||||
jobId: "job-1",
|
||||
status: "ok",
|
||||
summary: "newer run",
|
||||
sessionKey: "agent:main:cron:job-1:run:abc",
|
||||
},
|
||||
],
|
||||
onLoadRuns,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const selected = container.querySelector(".list-item-selected");
|
||||
expect(selected).not.toBeNull();
|
||||
|
||||
const row = container.querySelector(".list-item-clickable");
|
||||
expect(row).not.toBeNull();
|
||||
row?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onLoadRuns).toHaveBeenCalledWith("job-1");
|
||||
|
||||
const historyButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "History",
|
||||
);
|
||||
expect(historyButton).not.toBeUndefined();
|
||||
historyButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(onLoadRuns).toHaveBeenCalledTimes(2);
|
||||
expect(onLoadRuns).toHaveBeenNthCalledWith(1, "job-1");
|
||||
expect(onLoadRuns).toHaveBeenNthCalledWith(2, "job-1");
|
||||
|
||||
const link = container.querySelector("a.session-link");
|
||||
expect(link).not.toBeNull();
|
||||
expect(link?.getAttribute("href")).toContain(
|
||||
"/ui/chat?session=agent%3Amain%3Acron%3Ajob-1%3Arun%3Aabc",
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("Latest runs for Daily ping.");
|
||||
|
||||
const cards = Array.from(container.querySelectorAll(".card"));
|
||||
const runHistoryCard = cards.find(
|
||||
(card) => card.querySelector(".card-title")?.textContent?.trim() === "Run history",
|
||||
);
|
||||
expect(runHistoryCard).not.toBeUndefined();
|
||||
|
||||
const summaries = Array.from(
|
||||
runHistoryCard?.querySelectorAll(".list-item .list-sub") ?? [],
|
||||
).map((el) => (el.textContent ?? "").trim());
|
||||
expect(summaries[0]).toBe("newer run");
|
||||
expect(summaries[1]).toBe("older run");
|
||||
});
|
||||
|
||||
it("renders supported delivery options and normalizes stale announce selection", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
|
||||
@@ -65,7 +65,7 @@ function buildProps(result: SessionsListResult): SessionsProps {
|
||||
}
|
||||
|
||||
describe("sessions view", () => {
|
||||
it("keeps explicit and unknown session setting values selectable", async () => {
|
||||
it("keeps session selects stable and deselects only the current page", async () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderSessions(
|
||||
@@ -95,13 +95,10 @@ describe("sessions view", () => {
|
||||
expect(
|
||||
Array.from(reasoning?.options ?? []).some((option) => option.value === "custom-mode"),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("deselects only the current page from the header checkbox", async () => {
|
||||
const onSelectPage = vi.fn();
|
||||
const onDeselectPage = vi.fn();
|
||||
const onDeselectAll = vi.fn();
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderSessions({
|
||||
...buildProps(
|
||||
|
||||
Reference in New Issue
Block a user