fix(ui): unify Session navigation and restore uniform tab borders
This commit is contained in:
@@ -165,7 +165,9 @@ The full-mode application header matches Omics Portal's `--gsd-red-primary`
|
|||||||
use a near-white foreground. This header is absent in embedded mode. The sidebar
|
use a near-white foreground. This header is absent in embedded mode. The sidebar
|
||||||
and welcome wordmarks retain their red suffix. Context editing places workspace,
|
and welcome wordmarks retain their red suffix. Context editing places workspace,
|
||||||
model and Done in one desktop row, stacking on narrow containers. Session-scope
|
model and Done in one desktop row, stacking on narrow containers. Session-scope
|
||||||
tabs retain their selected fill and accessible keyboard state without a bottom border.
|
tabs retain their selected fill and accessible keyboard state with a uniform one-pixel
|
||||||
|
border on every side, gray when inactive and red when active. Their padding is 11px
|
||||||
|
horizontal and 3px vertical, with a 38px minimum height and wrapping labels.
|
||||||
|
|
||||||
The palette combines warm porcelain surfaces, warm graphite text, and an instrument red used only
|
The palette combines warm porcelain surfaces, warm graphite text, and an instrument red used only
|
||||||
for action, focus, and important state. OKLCH values in the frontmatter are normative because the
|
for action, focus, and important state. OKLCH values in the frontmatter are normative because the
|
||||||
@@ -306,6 +308,9 @@ default, hover, focus, active, disabled, loading, and error behavior where those
|
|||||||
|
|
||||||
### Navigation
|
### Navigation
|
||||||
|
|
||||||
|
- **Session entry:** one Session button returns to the current unfinished session,
|
||||||
|
including provisional creation, without resetting or reconnecting it. Otherwise
|
||||||
|
it prepares a new question using the normal readiness and unsaved-work guards.
|
||||||
- **Style:** compact session rows use `8px` corners and restrained vertical padding.
|
- **Style:** compact session rows use `8px` corners and restrained vertical padding.
|
||||||
- **Default / Hover / Active:** porcelain at rest, Sunken Surface on hover, and a muted Navigation
|
- **Default / Hover / Active:** porcelain at rest, Sunken Surface on hover, and a muted Navigation
|
||||||
Active red with a defined border when current. Exactly one top-level navigation control is current.
|
Active red with a defined border when current. Exactly one top-level navigation control is current.
|
||||||
@@ -324,7 +329,9 @@ default, hover, focus, active, disabled, loading, and error behavior where those
|
|||||||
### Tabs
|
### Tabs
|
||||||
|
|
||||||
- **Shape:** compact label tabs sit on a shared baseline with rounded top corners and a two-pixel
|
- **Shape:** compact label tabs sit on a shared baseline with rounded top corners and a two-pixel
|
||||||
lower edge, except session-scope tabs which have no bottom border. Inactive labels retain a Quiet Border and Porcelain Card surface, so every
|
lower edge, except session-scope tabs which use a uniform one-pixel border, rounded
|
||||||
|
corners and a 4px gap without a shared border or negative bottom margin.
|
||||||
|
Inactive labels retain a Quiet Border and Porcelain Card surface, so every
|
||||||
label reads as a tab before interaction; hover feedback reinforces clickability.
|
label reads as a tab before interaction; hover feedback reinforces clickability.
|
||||||
- **Current:** the selected tab uses the muted Navigation Active red for its fill, text, and defined border.
|
- **Current:** the selected tab uses the muted Navigation Active red for its fill, text, and defined border.
|
||||||
It must expose `aria-selected`, participate in a labelled `tablist`/`tabpanel`, and be the only
|
It must expose `aria-selected`, participate in a labelled `tablist`/`tabpanel`, and be the only
|
||||||
|
|||||||
+16
-1
@@ -48,6 +48,20 @@ PSD Gitea push and production deployment are still pending.
|
|||||||
|
|
||||||
## Current product shape
|
## Current product shape
|
||||||
|
|
||||||
|
### Unified Session entry and complete tab borders — 2026-09-13
|
||||||
|
|
||||||
|
The sidebar has one Session/Sessione button: return to the current unfinished
|
||||||
|
session (including pending creation) without resetting/reconnecting it; otherwise
|
||||||
|
prepare a new question with existing readiness and dirty-edit guards. Creation
|
||||||
|
still requires submitting a question. A cold document panel is not a running session.
|
||||||
|
Session tabs now have 11px horizontal/3px vertical padding, at least 38px height,
|
||||||
|
and matching 1px borders on every side (gray inactive, red active), with no shared
|
||||||
|
baseline or overlapping bottom border. This supersedes the earlier border removal.
|
||||||
|
757 frontend tests, 15 browser scenarios, typecheck, i18n and Docker build pass.
|
||||||
|
Mac frontend `2844778d311c`, image `d58dccfee3f9`, is healthy; only that service
|
||||||
|
was recreated. Core/data services are unchanged, with no Omics or server deploy.
|
||||||
|
Rollback image: `thothii-frontend:before-session-navigation-20260913`.
|
||||||
|
|
||||||
### Login copy and language-selector focus — 2026-09-13
|
### Login copy and language-selector focus — 2026-09-13
|
||||||
|
|
||||||
Removed the redundant login eyebrow/icon and installation-account explanation;
|
Removed the redundant login eyebrow/icon and installation-account explanation;
|
||||||
@@ -333,7 +347,8 @@ schema, Evidence, and vector mutation commands are retired.
|
|||||||
|
|
||||||
The right Administration sidebar invokes that same operation for the selected workspace. It shows
|
The right Administration sidebar invokes that same operation for the selected workspace. It shows
|
||||||
only current readiness or the latest bounded failure diagnostic; there is no preprocessing history.
|
only current readiness or the latest bounded failure diagnostic; there is no preprocessing history.
|
||||||
Known non-ready state disables **New session**, while backend admission remains authoritative.
|
Known non-ready state disables **Session** when it would start a new question,
|
||||||
|
while backend admission remains authoritative.
|
||||||
The same control exposes an inline-confirmed **Clear** action to remove replaceable reference
|
The same control exposes an inline-confirmed **Clear** action to remove replaceable reference
|
||||||
vectors, LSH, corpus, and checkpoints while preserving the separate Memory collection. The host CLI
|
vectors, LSH, corpus, and checkpoints while preserving the separate Memory collection. The host CLI
|
||||||
equivalent is `workspace preprocess clear`.
|
equivalent is `workspace preprocess clear`.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ the end of this page.
|
|||||||
## Before creating a session
|
## Before creating a session
|
||||||
|
|
||||||
An administrator must have selected a workspace and configured the installation-wide provider,
|
An administrator must have selected a workspace and configured the installation-wide provider,
|
||||||
model, and thinking settings. The New session form deliberately asks only for the question.
|
model, and thinking settings. The new-question form deliberately asks only for the question.
|
||||||
|
|
||||||
The workspace is a pinned Git revision. A subsequent workspace update cannot alter a session
|
The workspace is a pinned Git revision. A subsequent workspace update cannot alter a session
|
||||||
already created from an earlier revision. If a workspace cannot reach its configured runtime DWH,
|
already created from an earlier revision. If a workspace cannot reach its configured runtime DWH,
|
||||||
@@ -15,7 +15,9 @@ new sessions are refused before any session state is written.
|
|||||||
|
|
||||||
## Create and review a session
|
## Create and review a session
|
||||||
|
|
||||||
1. Sign in and select **New session**.
|
1. Sign in and select **Session** (**Sessione** in Italian). If a session is already
|
||||||
|
open and unfinished, this returns to it without restarting it. Otherwise it
|
||||||
|
opens a new question; no session is created until you submit that question.
|
||||||
2. Enter a precise business question, including the relevant time period and desired output. For
|
2. Enter a precise business question, including the relevant time period and desired output. For
|
||||||
example: “List patients discharged in the last 30 days, with ward and discharge date.”
|
example: “List patients discharged in the last 30 days, with ward and discharge date.”
|
||||||
3. Review each gate and make the decision requested by the widget. A choice with a decision payload
|
3. Review each gate and make the decision requested by the widget. A choice with a decision payload
|
||||||
|
|||||||
@@ -73,3 +73,33 @@ Solo il frontend Mac è stato ricreato, healthy: container `204efd40d3eb`, immag
|
|||||||
Core, catalogo, Qdrant ed embedding sono invariati. Per annullare soltanto questo
|
Core, catalogo, Qdrant ed embedding sono invariati. Per annullare soltanto questo
|
||||||
follow-up, usare nei comandi di rollback sopra il tag
|
follow-up, usare nei comandi di rollback sopra il tag
|
||||||
`thothii-frontend:before-login-focus-20260913`.
|
`thothii-frontend:before-login-focus-20260913`.
|
||||||
|
|
||||||
|
## Follow-up: comando Sessione unico e bordi completi
|
||||||
|
|
||||||
|
Il pulsante Session/Sessione sostituisce New session e Return to session. Una
|
||||||
|
sessione aperta non finalizzata/archiviata, o una creazione già inviata in attesa
|
||||||
|
di completamento, viene conservata e riportata in primo piano. Nessun reset,
|
||||||
|
nuova ripresa o ricreazione della connessione eventi. In assenza di sessione in
|
||||||
|
corso si prepara una nuova domanda attraverso il percorso esistente: controlli
|
||||||
|
di contesto/preprocessing, protezione delle modifiche amministrative, prewarm e
|
||||||
|
focus del campo. La sessione viene creata solo all'invio della domanda. Un pannello
|
||||||
|
documentale non ripreso può essere riaperto dalla relativa voce nella lista.
|
||||||
|
|
||||||
|
I tab ricevono 3px aggiuntivi per lato orizzontale e 3px verticali, con altezza
|
||||||
|
minima 38px e crescita per le etichette italiane su due righe. Tutti i bordi sono
|
||||||
|
da 1px e dello stesso colore: grigio inattivo, rosso attivo. Rimossi il bordo
|
||||||
|
del contenitore e il margine negativo; 4px separano i tab. Questa richiesta
|
||||||
|
sostituisce la precedente rimozione del bordo inferiore.
|
||||||
|
|
||||||
|
Suite frontend completa: 757 test in 90 file; Playwright: 15 scenari. Superati
|
||||||
|
anche typecheck, controllo di 1683 traduzioni italiane/1703 riferimenti statici
|
||||||
|
e build Docker. Coperti ritorno senza seconda ripresa, clic durante creazione,
|
||||||
|
assenza di creazione prima dell'invio, bozze amministrative e del composer,
|
||||||
|
geometria/bordi EN/IT in chiaro e scuro e layout mobile/embedded. Le fixture
|
||||||
|
bloccano le chiamate reali, incluso il prewarm. Screenshot italiani verificati.
|
||||||
|
Impeccable ha guidato la spaziatura e la conservazione dello stato accessibile.
|
||||||
|
|
||||||
|
Il rollback di questo follow-up usa il tag
|
||||||
|
`thothii-frontend:before-session-navigation-20260913` con i comandi sopra.
|
||||||
|
Frontend Mac aggiornato e healthy: container `2844778d311c`, immagine `d58dccfee3f9`.
|
||||||
|
Core e servizi dati rimangono quelli registrati sopra; nessun deploy Omics/PSD.
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ test("context panels stay inside the manager and the Tables grid sits in a sideb
|
|||||||
|
|
||||||
await page.goto(stack.publicUrl);
|
await page.goto(stack.publicUrl);
|
||||||
await signInAsAdmin(page);
|
await signInAsAdmin(page);
|
||||||
await expectCurrentNavigation(page, "New session");
|
await expectCurrentNavigation(page, "Session");
|
||||||
await expectSelectedSessionScopeTab(page, "My sessions");
|
await expectSelectedSessionScopeTab(page, "My sessions");
|
||||||
await page.getByRole("tab", { name: "All sessions", exact: true }).click();
|
await page.getByRole("tab", { name: "All sessions", exact: true }).click();
|
||||||
await expectSelectedSessionScopeTab(page, "All sessions");
|
await expectSelectedSessionScopeTab(page, "All sessions");
|
||||||
@@ -808,7 +808,7 @@ test("Workspace and Pi management share the centered work-area panel without cov
|
|||||||
.getByRole("button", { name: "Close Pi management" })
|
.getByRole("button", { name: "Close Pi management" })
|
||||||
.click();
|
.click();
|
||||||
await expect(piTrigger).toBeFocused();
|
await expect(piTrigger).toBeFocused();
|
||||||
await expectCurrentNavigation(page, "New session");
|
await expectCurrentNavigation(page, "Session");
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.setViewportSize({ width: 720, height: 800 });
|
await page.setViewportSize({ width: 720, height: 800 });
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ test("F1 loop: new question → F1 widget → respond", async ({ page }) => {
|
|||||||
await signInLocally(page);
|
await signInLocally(page);
|
||||||
|
|
||||||
// Focus the composer for a new session.
|
// Focus the composer for a new session.
|
||||||
await page.getByRole("button", { name: "New session", exact: true }).click();
|
await page.getByRole("button", { name: "Session", exact: true }).click();
|
||||||
|
|
||||||
// Fill in the question.
|
// Fill in the question.
|
||||||
await page.getByLabel("New question").fill("quante cardioversioni nel 2024");
|
await page.getByLabel("New question").fill("quante cardioversioni nel 2024");
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ for (const width of [390, 1280]) {
|
|||||||
expect(Math.abs(modelBox!.y - doneBox!.y)).toBeLessThan(1);
|
expect(Math.abs(modelBox!.y - doneBox!.y)).toBeLessThan(1);
|
||||||
expect(doneBox!.x).toBeGreaterThan(modelBox!.x + modelBox!.width);
|
expect(doneBox!.x).toBeGreaterThan(modelBox!.x + modelBox!.width);
|
||||||
for (const name of ["My sessions", "All sessions"]) {
|
for (const name of ["My sessions", "All sessions"]) {
|
||||||
await expect(page.getByRole("tab", { name, exact: true })).toHaveCSS("border-bottom-width", "0px");
|
await expect(page.getByRole("tab", { name, exact: true })).toHaveCSS("border-bottom-width", "1px");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] });
|
expect(await inspect(page)).toEqual({ overflow: 0, serif: [], tiny: [] });
|
||||||
@@ -162,6 +162,42 @@ for (const width of [390, 1280]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("session tabs have padded labels and matching one-pixel borders on every side", async ({ page }, testInfo) => {
|
||||||
|
await page.setViewportSize({ width: 1280, height: 1000 });
|
||||||
|
await fixtures(page);
|
||||||
|
await page.goto("/?thoth_route=administration%2Fdatabase");
|
||||||
|
const header = page.locator(".thot-full-header");
|
||||||
|
for (const locale of ["en", "it"]) {
|
||||||
|
await header.getByRole("combobox").selectOption(locale);
|
||||||
|
await expect(page.getByRole("button", { name: locale === "it" ? "Sessione" : "Session", exact: true })).toHaveCount(1);
|
||||||
|
await expect(page.getByRole("button", { name: /^(New session|Return to session|Nuova sessione|Torna alla sessione)$/ })).toHaveCount(0);
|
||||||
|
const tabs = page.locator('[role="tablist"]').getByRole("tab");
|
||||||
|
for (const theme of ["light", "dark"]) {
|
||||||
|
if (theme === "dark") await header.getByRole("button", { name: locale === "it" ? "Usa il tema scuro" : "Use dark theme", exact: true }).click();
|
||||||
|
for (const selected of [0, 1]) {
|
||||||
|
await tabs.nth(selected).click();
|
||||||
|
await expect(tabs.nth(selected)).toHaveAttribute("aria-selected", "true");
|
||||||
|
for (const tab of await tabs.all()) {
|
||||||
|
await expect(tab).toHaveCSS("border-bottom-width", "1px");
|
||||||
|
const borders = await tab.evaluate(el => {
|
||||||
|
const css = getComputedStyle(el);
|
||||||
|
return { widths: [css.borderTopWidth, css.borderRightWidth, css.borderBottomWidth, css.borderLeftWidth],
|
||||||
|
colors: [css.borderTopColor, css.borderRightColor, css.borderBottomColor, css.borderLeftColor],
|
||||||
|
paddingX: css.paddingLeft, paddingY: css.paddingTop,
|
||||||
|
fits: el.scrollHeight <= el.clientHeight };
|
||||||
|
});
|
||||||
|
expect(borders.widths).toEqual(["1px", "1px", "1px", "1px"]);
|
||||||
|
expect(new Set(borders.colors).size).toBe(1);
|
||||||
|
expect(borders).toMatchObject({ paddingX: "11px", paddingY: "3px", fits: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await page.mouse.move(0, 0);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath(`session-tabs-${locale}-${theme}.png`), animations: "disabled" });
|
||||||
|
if (theme === "dark") await header.getByRole("button", { name: locale === "it" ? "Usa il tema chiaro" : "Use light theme", exact: true }).click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for (const locale of ["en", "it"]) {
|
for (const locale of ["en", "it"]) {
|
||||||
test(`login keeps only the main title in ${locale}`, async ({ page }, testInfo) => {
|
test(`login keeps only the main title in ${locale}`, async ({ page }, testInfo) => {
|
||||||
await fixtures(page);
|
await fixtures(page);
|
||||||
@@ -202,7 +238,7 @@ for (const route of ["/", "/?thoth_route=administration%2Fdatabase"]) {
|
|||||||
await page.goto(route);
|
await page.goto(route);
|
||||||
if (route !== "/") {
|
if (route !== "/") {
|
||||||
await navigation(page);
|
await navigation(page);
|
||||||
await page.getByRole("button", { name: "Return to session", exact: true }).click();
|
await page.getByRole("button", { name: "Session", exact: true }).click();
|
||||||
}
|
}
|
||||||
const prompt = page.getByRole("textbox", { name: "New question", exact: true });
|
const prompt = page.getByRole("textbox", { name: "New question", exact: true });
|
||||||
await expect(prompt).toBeVisible();
|
await expect(prompt).toBeVisible();
|
||||||
@@ -234,14 +270,14 @@ for (const route of ["/", "/?thoth_route=administration%2Fdatabase"]) {
|
|||||||
expect(await prompt.evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true);
|
expect(await prompt.evaluate(el => el.scrollHeight > el.clientHeight)).toBe(true);
|
||||||
await admin(page, "Database");
|
await admin(page, "Database");
|
||||||
await navigation(page);
|
await navigation(page);
|
||||||
await page.getByRole("button", { name: "Return to session", exact: true }).click();
|
await page.getByRole("button", { name: "Session", exact: true }).click();
|
||||||
await expect(prompt).toHaveValue(question);
|
await expect(prompt).toHaveValue(question);
|
||||||
await readable();
|
await readable();
|
||||||
await page.screenshot({ path: testInfo.outputPath("Prompt-multiline-mobile.png") });
|
await page.screenshot({ path: testInfo.outputPath("Prompt-multiline-mobile.png") });
|
||||||
await prompt.fill("");
|
await prompt.fill("");
|
||||||
await readable();
|
await readable();
|
||||||
await expect.poll(() => prompt.evaluate(el => el.clientHeight)).toBe(emptyHeight);
|
await expect.poll(() => prompt.evaluate(el => el.clientHeight)).toBe(emptyHeight);
|
||||||
expect(writes).toEqual([]);
|
expect(writes).toEqual(route === "/" ? ["/runtime/prewarm"] : ["/runtime/prewarm", "/runtime/prewarm"]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,13 +397,13 @@ test("theme follows the app, not an unrelated OS preference; keyboard tabs and d
|
|||||||
await admin(page, "Memory");
|
await admin(page, "Memory");
|
||||||
await page.getByRole("button", { name: "New card", exact: true }).click();
|
await page.getByRole("button", { name: "New card", exact: true }).click();
|
||||||
await page.getByRole("textbox", { name: "Title", exact: true }).fill("Keep this draft");
|
await page.getByRole("textbox", { name: "Title", exact: true }).fill("Keep this draft");
|
||||||
await page.getByRole("button", { name: "Return to session", exact: true }).click();
|
await page.getByRole("button", { name: "Session", exact: true }).click();
|
||||||
await expect(page.getByRole("textbox", { name: "Title", exact: true })).toHaveValue("Keep this draft");
|
await expect(page.getByRole("textbox", { name: "Title", exact: true })).toHaveValue("Keep this draft");
|
||||||
await page.screenshot({ path: testInfo.outputPath("Memory-dark-unsaved.png") });
|
await page.screenshot({ path: testInfo.outputPath("Memory-dark-unsaved.png") });
|
||||||
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||||
await page.getByRole("button", { name: "Return to session", exact: true }).click();
|
await page.getByRole("button", { name: "Session", exact: true }).click();
|
||||||
await expect(page.getByRole("textbox", { name: "New question", exact: true })).toBeVisible();
|
await expect(page.getByRole("textbox", { name: "New question", exact: true })).toBeVisible();
|
||||||
expect(writes).toEqual([]);
|
expect(writes).toEqual(["/runtime/prewarm"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("host header and left rail leave the application a bounded working area", async ({ page }, testInfo) => {
|
test("host header and left rail leave the application a bounded working area", async ({ page }, testInfo) => {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ test("local authentication renders the standalone logout control", async () => {
|
|||||||
|
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
expect(await screen.findByRole("button", { name: "New session" })).toBeInTheDocument();
|
expect(await screen.findByRole("button", { name: "Session" })).toBeInTheDocument();
|
||||||
expect(screen.getByText("Portal user")).toBeInTheDocument();
|
expect(screen.getByText("Portal user")).toBeInTheDocument();
|
||||||
await userEvent.click(within(screen.getByRole("banner", { name: "Application header" })).getByRole("button", { name: "Portal user" }));
|
await userEvent.click(within(screen.getByRole("banner", { name: "Application header" })).getByRole("button", { name: "Portal user" }));
|
||||||
expect(await screen.findByRole("menuitem", { name: "Log out" })).toBeInTheDocument();
|
expect(await screen.findByRole("menuitem", { name: "Log out" })).toBeInTheDocument();
|
||||||
@@ -113,7 +113,7 @@ test("a local login always enters the core activity", async () => {
|
|||||||
await userEvent.type(screen.getByLabelText(/^password$/i), "correct-password");
|
await userEvent.type(screen.getByLabelText(/^password$/i), "correct-password");
|
||||||
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
await userEvent.click(screen.getByRole("button", { name: /sign in/i }));
|
||||||
|
|
||||||
expect(await screen.findByRole("button", { name: "New session" }))
|
expect(await screen.findByRole("button", { name: "Session" }))
|
||||||
.toHaveAttribute("aria-current", "page");
|
.toHaveAttribute("aria-current", "page");
|
||||||
expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export const itCore: Record<string, string> = {
|
|||||||
"Human-in-the-loop review": "revisione umana",
|
"Human-in-the-loop review": "revisione umana",
|
||||||
"Return to session": "Torna alla sessione",
|
"Return to session": "Torna alla sessione",
|
||||||
"New session": "Nuova sessione",
|
"New session": "Nuova sessione",
|
||||||
|
"Session": "Sessione",
|
||||||
|
"Start with Session.": "Inizia con Sessione.",
|
||||||
"Administration": "Amministrazione",
|
"Administration": "Amministrazione",
|
||||||
"Database management permission is required": "È richiesto il permesso di gestione dei database",
|
"Database management permission is required": "È richiesto il permesso di gestione dei database",
|
||||||
"Database": "Database",
|
"Database": "Database",
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ test("Back and Core navigation preserve a Memory draft until the editor cancels
|
|||||||
act(() => window.history.back());
|
act(() => window.history.back());
|
||||||
await waitFor(() => expect(new URLSearchParams(window.location.search).get("thoth_route")).toBe("administration/memory"));
|
await waitFor(() => expect(new URLSearchParams(window.location.search).get("thoth_route")).toBe("administration/memory"));
|
||||||
expect(within(page).getByLabelText("Title")).toHaveValue("Keep this draft");
|
expect(within(page).getByLabelText("Title")).toHaveValue("Keep this draft");
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Return to session" }));
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
expect(page).toBeVisible();
|
expect(page).toBeVisible();
|
||||||
expect(confirm).not.toHaveBeenCalled();
|
expect(confirm).not.toHaveBeenCalled();
|
||||||
confirm.mockReturnValue(true);
|
confirm.mockReturnValue(true);
|
||||||
@@ -138,7 +138,7 @@ test("option A has one collapsible global context and preserves the Core draft a
|
|||||||
await userEvent.click(screen.getByRole("button", { name: "Memory" }));
|
await userEvent.click(screen.getByRole("button", { name: "Memory" }));
|
||||||
expect(await screen.findByRole("main", { name: "Memory management" })).toBeVisible();
|
expect(await screen.findByRole("main", { name: "Memory management" })).toBeVisible();
|
||||||
expect(screen.queryByRole("textbox", { name: /new question/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole("textbox", { name: /new question/i })).not.toBeInTheDocument();
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Return to session" }));
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
expect(screen.getByRole("textbox", { name: /new question/i })).toBe(composer);
|
expect(screen.getByRole("textbox", { name: /new question/i })).toBe(composer);
|
||||||
expect(composer).toHaveValue("Keep my original Core question");
|
expect(composer).toHaveValue("Keep my original Core question");
|
||||||
});
|
});
|
||||||
@@ -149,7 +149,7 @@ test("invalid remembered choices do not silently fall back and gate both Core an
|
|||||||
window.history.replaceState(null, "", "/?thoth_route=administration/memory");
|
window.history.replaceState(null, "", "/?thoth_route=administration/memory");
|
||||||
renderShell();
|
renderShell();
|
||||||
await screen.findByText(/Unavailable saved choices are not replaced/);
|
await screen.findByText(/Unavailable saved choices are not replaced/);
|
||||||
expect(screen.getByRole("button", { name: "New session" })).toBeDisabled();
|
expect(screen.getByRole("button", { name: "Session" })).toBeDisabled();
|
||||||
expect(screen.queryByRole("main", { name: "Memory management" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("main", { name: "Memory management" })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("textbox", { name: /new question/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole("textbox", { name: /new question/i })).not.toBeInTheDocument();
|
||||||
await userEvent.selectOptions(screen.getByRole("combobox", { name: "Workspace" }), "alpha");
|
await userEvent.selectOptions(screen.getByRole("combobox", { name: "Workspace" }), "alpha");
|
||||||
@@ -193,7 +193,7 @@ test("dirty Memory blocks workspace and model changes, including after a failed
|
|||||||
await userEvent.click(within(page).getByRole("button", { name: "Save card" }));
|
await userEvent.click(within(page).getByRole("button", { name: "Save card" }));
|
||||||
await waitFor(() => expect(saveAttempts).toBe(1));
|
await waitFor(() => expect(saveAttempts).toBe(1));
|
||||||
expect(await within(page).findByRole("alert")).toBeVisible();
|
expect(await within(page).findByRole("alert")).toBeVisible();
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Return to session" }));
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
expect(page).toBeVisible();
|
expect(page).toBeVisible();
|
||||||
expect(within(page).getByLabelText("Title")).toHaveValue("Do not lose this");
|
expect(within(page).getByLabelText("Title")).toHaveValue("Do not lose this");
|
||||||
});
|
});
|
||||||
@@ -245,12 +245,12 @@ test("a successful Memory save releases the navigation guard and the context loc
|
|||||||
await userEvent.click(screen.getByRole("button", { name: /Working context/ }));
|
await userEvent.click(screen.getByRole("button", { name: /Working context/ }));
|
||||||
await userEvent.click(within(page).getByRole("button", { name: "Save card" }));
|
await userEvent.click(within(page).getByRole("button", { name: "Save card" }));
|
||||||
expect(screen.getByRole("combobox", { name: "Model" })).toBeDisabled();
|
expect(screen.getByRole("combobox", { name: "Model" })).toBeDisabled();
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Return to session" }));
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
expect(page).toBeVisible();
|
expect(page).toBeVisible();
|
||||||
await act(async () => release());
|
await act(async () => release());
|
||||||
await within(page).findByText("Saved and indexed.");
|
await within(page).findByText("Saved and indexed.");
|
||||||
expect(screen.getByRole("combobox", { name: "Model" })).toBeEnabled();
|
expect(screen.getByRole("combobox", { name: "Model" })).toBeEnabled();
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Return to session" }));
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
expect(await screen.findByRole("textbox", { name: /new question/i })).toBeVisible();
|
expect(await screen.findByRole("textbox", { name: /new question/i })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ describe("authenticated shell permissions", () => {
|
|||||||
test("hides administrative navigation from a session user", async () => {
|
test("hides administrative navigation from a session user", async () => {
|
||||||
renderShell({ subject: "user-1", isAdmin: false, roles: ["user"], permissions: ["session.use"] });
|
renderShell({ subject: "user-1", isAdmin: false, roles: ["user"], permissions: ["session.use"] });
|
||||||
|
|
||||||
expect(await screen.findByRole("button", { name: "New session" })).toBeInTheDocument();
|
expect(await screen.findByRole("button", { name: "Session" })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Database" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Database" })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Workspace" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Workspace" })).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ test("starts on the core activity after a hard refresh even if metadata manageme
|
|||||||
await expandAdministration();
|
await expandAdministration();
|
||||||
|
|
||||||
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
||||||
expect(within(sessionNavigation).getByRole("button", { name: "New session" }))
|
expect(within(sessionNavigation).getByRole("button", { name: "Session" }))
|
||||||
.toHaveAttribute("aria-current", "page");
|
.toHaveAttribute("aria-current", "page");
|
||||||
expect(within(sessionNavigation).getByRole("button", { name: "Database" }))
|
expect(within(sessionNavigation).getByRole("button", { name: "Database" }))
|
||||||
.not.toHaveAttribute("aria-current");
|
.not.toHaveAttribute("aria-current");
|
||||||
@@ -118,7 +118,7 @@ test("replaces the core conversation while keeping the session navigation", asyn
|
|||||||
|
|
||||||
const conversationColumn = screen.getByTestId("conversation-column");
|
const conversationColumn = screen.getByTestId("conversation-column");
|
||||||
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
||||||
const newSession = within(sessionNavigation).getByRole("button", { name: "New session" });
|
const newSession = within(sessionNavigation).getByRole("button", { name: "Session" });
|
||||||
const databaseManagement = within(sessionNavigation).getByRole("button", { name: "Database" });
|
const databaseManagement = within(sessionNavigation).getByRole("button", { name: "Database" });
|
||||||
|
|
||||||
expect(newSession).toHaveAttribute("aria-current", "page");
|
expect(newSession).toHaveAttribute("aria-current", "page");
|
||||||
@@ -146,6 +146,7 @@ test("replaces the core conversation while keeping the session navigation", asyn
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("keeps a live core session connected while returning from database management", async () => {
|
test("keeps a live core session connected while returning from database management", async () => {
|
||||||
|
let resumes = 0;
|
||||||
server.use(
|
server.use(
|
||||||
http.get("/api/sessions", () => HttpResponse.json([{
|
http.get("/api/sessions", () => HttpResponse.json([{
|
||||||
id: "s1",
|
id: "s1",
|
||||||
@@ -160,10 +161,10 @@ test("keeps a live core session connected while returning from database manageme
|
|||||||
archived: false,
|
archived: false,
|
||||||
active: true,
|
active: true,
|
||||||
}])),
|
}])),
|
||||||
http.post("/api/sessions/:id/resume", ({ params }) => HttpResponse.json({
|
http.post("/api/sessions/:id/resume", ({ params }) => { resumes++; return HttpResponse.json({
|
||||||
id: params.id,
|
id: params.id,
|
||||||
alreadyActive: true,
|
alreadyActive: true,
|
||||||
})),
|
}); }),
|
||||||
http.get("/api/sessions/:id", ({ params }) => HttpResponse.json({
|
http.get("/api/sessions/:id", ({ params }) => HttpResponse.json({
|
||||||
id: params.id,
|
id: params.id,
|
||||||
status: "open",
|
status: "open",
|
||||||
@@ -185,13 +186,19 @@ test("keeps a live core session connected while returning from database manageme
|
|||||||
expect(FakeEventSource.instances).toHaveLength(1);
|
expect(FakeEventSource.instances).toHaveLength(1);
|
||||||
|
|
||||||
expect(session).toBeVisible();
|
expect(session).toBeVisible();
|
||||||
await userEvent.click(session);
|
expect(screen.queryByRole("button", { name: "Return to session" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("button", { name: "New session" })).not.toBeInTheDocument();
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("main", { name: "Database management" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
expect(source.closed).toBe(false);
|
expect(source.closed).toBe(false);
|
||||||
expect(FakeEventSource.instances).toHaveLength(1);
|
expect(FakeEventSource.instances).toHaveLength(1);
|
||||||
|
expect(resumes).toBe(1);
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
|
expect(source.closed).toBe(false);
|
||||||
|
expect(resumes).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("hides the complete administrative navigation from a regular user", () => {
|
test("hides the complete administrative navigation from a regular user", () => {
|
||||||
@@ -208,7 +215,7 @@ test("hides the complete administrative navigation from a regular user", () => {
|
|||||||
|
|
||||||
renderShell();
|
renderShell();
|
||||||
|
|
||||||
expect(screen.getByRole("button", { name: "New session" })).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: "Session" })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Workspace" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Workspace" })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: "Database" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Database" })).not.toBeInTheDocument();
|
||||||
@@ -256,7 +263,7 @@ test("requires saving or cancelling a dirty database form before leaving", async
|
|||||||
await userEvent.clear(schema);
|
await userEvent.clear(schema);
|
||||||
await userEvent.type(schema, "reporting");
|
await userEvent.type(schema, "reporting");
|
||||||
|
|
||||||
const newSession = screen.getByRole("button", { name: "New session" });
|
const newSession = screen.getByRole("button", { name: "Session" });
|
||||||
await userEvent.click(newSession);
|
await userEvent.click(newSession);
|
||||||
|
|
||||||
expect(confirm).not.toHaveBeenCalled();
|
expect(confirm).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -47,9 +47,11 @@ beforeEach(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("New session starts prewarm without delaying composer focus", async () => {
|
test("Session prepares a new question without creating a session before submission", async () => {
|
||||||
let prewarmStarted = false;
|
let prewarmStarted = false;
|
||||||
|
const create = vi.fn(() => HttpResponse.json({ id: "unexpected" }));
|
||||||
server.use(
|
server.use(
|
||||||
|
http.post("/api/sessions", create),
|
||||||
http.post("/api/runtime/prewarm", async () => {
|
http.post("/api/runtime/prewarm", async () => {
|
||||||
prewarmStarted = true;
|
prewarmStarted = true;
|
||||||
await delay(100);
|
await delay(100);
|
||||||
@@ -58,16 +60,18 @@ test("New session starts prewarm without delaying composer focus", async () => {
|
|||||||
);
|
);
|
||||||
renderShell();
|
renderShell();
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: /^new session$/i }));
|
await userEvent.click(screen.getByRole("button", { name: /^Session$/i }));
|
||||||
|
|
||||||
const composer = await screen.findByRole("textbox", { name: /new question/i });
|
const composer = await screen.findByRole("textbox", { name: /new question/i });
|
||||||
await waitFor(() => expect(prewarmStarted).toBe(true));
|
await waitFor(() => expect(prewarmStarted).toBe(true));
|
||||||
await waitFor(() => expect(composer).toHaveFocus());
|
await waitFor(() => expect(composer).toHaveFocus());
|
||||||
expect(composer).toHaveAttribute("data-awaiting-input", "true");
|
expect(composer).toHaveAttribute("data-awaiting-input", "true");
|
||||||
expect(composer).toHaveClass("thot-awaiting-input");
|
expect(composer).toHaveClass("thot-awaiting-input");
|
||||||
|
expect(create).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getAllByRole("button", { name: "Session" })).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a known preprocessing requirement disables New session", async () => {
|
test("a known preprocessing requirement disables Session when no session is open", async () => {
|
||||||
server.use(
|
server.use(
|
||||||
http.get("/api/workspaces/default/preprocessing", () => HttpResponse.json({
|
http.get("/api/workspaces/default/preprocessing", () => HttpResponse.json({
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
@@ -81,7 +85,7 @@ test("a known preprocessing requirement disables New session", async () => {
|
|||||||
);
|
);
|
||||||
renderShell();
|
renderShell();
|
||||||
|
|
||||||
const newSession = screen.getByRole("button", { name: "New session" });
|
const newSession = screen.getByRole("button", { name: "Session" });
|
||||||
await waitFor(() => expect(newSession).toBeDisabled());
|
await waitFor(() => expect(newSession).toBeDisabled());
|
||||||
expect(newSession).toHaveAttribute("data-navigation-state", "unavailable");
|
expect(newSession).toHaveAttribute("data-navigation-state", "unavailable");
|
||||||
await waitFor(() => expect(newSession).toHaveAttribute("title", "Catalog revision 18 is not indexed."));
|
await waitFor(() => expect(newSession).toHaveAttribute("title", "Catalog revision 18 is not indexed."));
|
||||||
@@ -110,6 +114,10 @@ test("records the prompt without central duplication, then opens the live log",
|
|||||||
phase: "F1",
|
phase: "F1",
|
||||||
text: "How many patients?",
|
text: "How many patients?",
|
||||||
});
|
});
|
||||||
|
// Clicking the unified navigation while creation is pending must not discard
|
||||||
|
// its operation token or the provisional transcript.
|
||||||
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
|
expect(useSessionStore.getState().activityLog[0]?.text).toBe("How many patients?");
|
||||||
releaseCreate();
|
releaseCreate();
|
||||||
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
||||||
act(() => FakeEventSource.instances[0].emit({ type: "text_delta", text: "Inspecting cohort" }));
|
act(() => FakeEventSource.instances[0].emit({ type: "text_delta", text: "Inspecting cohort" }));
|
||||||
@@ -211,7 +219,7 @@ test("opens Workspace management from the right sidebar without interrupting the
|
|||||||
const workArea = screen.getByTestId("conversation-column");
|
const workArea = screen.getByTestId("conversation-column");
|
||||||
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
||||||
const workspaceManagement = within(sessionNavigation).getByRole("button", { name: "Workspace" });
|
const workspaceManagement = within(sessionNavigation).getByRole("button", { name: "Workspace" });
|
||||||
const newSession = within(sessionNavigation).getByRole("button", { name: "New session" });
|
const newSession = within(sessionNavigation).getByRole("button", { name: "Session" });
|
||||||
expect(workArea).toContainElement(dialog);
|
expect(workArea).toContainElement(dialog);
|
||||||
expect(sessionNavigation).not.toContainElement(dialog);
|
expect(sessionNavigation).not.toContainElement(dialog);
|
||||||
expect(screen.getByTestId("app-shell")).toHaveAttribute("data-activity-layout", "closed");
|
expect(screen.getByTestId("app-shell")).toHaveAttribute("data-activity-layout", "closed");
|
||||||
@@ -222,7 +230,7 @@ test("opens Workspace management from the right sidebar without interrupting the
|
|||||||
expect(newSession).toHaveAttribute("data-navigation-state", "available");
|
expect(newSession).toHaveAttribute("data-navigation-state", "available");
|
||||||
expect(newSession).not.toHaveAttribute("aria-current");
|
expect(newSession).not.toHaveAttribute("aria-current");
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole("button", { name: "Return to session" }));
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
await waitFor(() => expect(screen.queryByRole("main", { name: "Workspace management" })).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByRole("main", { name: "Workspace management" })).not.toBeInTheDocument());
|
||||||
expect(newSession).toHaveAttribute("aria-current", "page");
|
expect(newSession).toHaveAttribute("aria-current", "page");
|
||||||
expect(newSession).toHaveAttribute("data-navigation-state", "current");
|
expect(newSession).toHaveAttribute("data-navigation-state", "current");
|
||||||
@@ -240,7 +248,7 @@ test("does not block the shell when the DWH is unavailable at startup", async ()
|
|||||||
);
|
);
|
||||||
renderShell();
|
renderShell();
|
||||||
|
|
||||||
expect(await screen.findByRole("button", { name: "New session" })).toBeVisible();
|
expect(await screen.findByRole("button", { name: "Session" })).toBeVisible();
|
||||||
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Administration" })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("heading", { name: "Connection unavailable" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("heading", { name: "Connection unavailable" })).not.toBeInTheDocument();
|
||||||
expect(healthChecks).toBe(0);
|
expect(healthChecks).toBe(0);
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ test("Pi management preserves the open session summary and the model activity ti
|
|||||||
const workArea = screen.getByTestId("conversation-column");
|
const workArea = screen.getByTestId("conversation-column");
|
||||||
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
const sessionNavigation = screen.getByRole("complementary", { name: "Session navigation" });
|
||||||
const piManagement = within(sessionNavigation).getByRole("button", { name: "Pi configuration" });
|
const piManagement = within(sessionNavigation).getByRole("button", { name: "Pi configuration" });
|
||||||
const newSession = within(sessionNavigation).getByRole("button", { name: "New session" });
|
const newSession = within(sessionNavigation).getByRole("button", { name: "Session" });
|
||||||
expect(workArea).toContainElement(dialog);
|
expect(workArea).toContainElement(dialog);
|
||||||
expect(sessionNavigation).not.toContainElement(dialog);
|
expect(sessionNavigation).not.toContainElement(dialog);
|
||||||
expect(piManagement).toHaveAttribute("aria-current", "page");
|
expect(piManagement).toHaveAttribute("aria-current", "page");
|
||||||
@@ -147,7 +147,9 @@ test("Pi management preserves the open session summary and the model activity ti
|
|||||||
const preservedSummary = document.querySelector('[aria-label="Session summary"]');
|
const preservedSummary = document.querySelector('[aria-label="Session summary"]');
|
||||||
expect(preservedSummary).toHaveAttribute("aria-hidden", "true");
|
expect(preservedSummary).toHaveAttribute("aria-hidden", "true");
|
||||||
expect(preservedSummary).toHaveTextContent("Attiva uno");
|
expect(preservedSummary).toHaveTextContent("Attiva uno");
|
||||||
await user.click(screen.getByRole("button", { name: "Return to session" }));
|
// A cold document panel is not a running session. Reopen it from its row;
|
||||||
|
// the unified Session action would prepare a new question instead.
|
||||||
|
await user.click(screen.getByTestId("session-item-s1"));
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByRole("complementary", { name: "Session summary" })).toHaveTextContent("Domanda originale");
|
expect(screen.getByRole("complementary", { name: "Session summary" })).toHaveTextContent("Domanda originale");
|
||||||
});
|
});
|
||||||
@@ -160,7 +162,7 @@ test("Pi management preserves the open session summary and the model activity ti
|
|||||||
await screen.findByRole("button", { name: "Show model activity" });
|
await screen.findByRole("button", { name: "Show model activity" });
|
||||||
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Preserved activity" }));
|
act(() => useSessionStore.getState().setLastUserEntry({ kind: "input", text: "Preserved activity" }));
|
||||||
await user.click(screen.getByRole("button", { name: "Pi configuration" }));
|
await user.click(screen.getByRole("button", { name: "Pi configuration" }));
|
||||||
await user.click(await screen.findByRole("button", { name: "Return to session" }));
|
await user.click(await screen.findByRole("button", { name: "Session" }));
|
||||||
await waitFor(() => expect(screen.queryByRole("heading", { name: "Pi management" })).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByRole("heading", { name: "Pi management" })).not.toBeInTheDocument());
|
||||||
await user.click(screen.getByRole("button", { name: "Show model activity" }));
|
await user.click(screen.getByRole("button", { name: "Show model activity" }));
|
||||||
expect(await screen.findByRole("heading", { name: "Model activity" })).toBeVisible();
|
expect(await screen.findByRole("heading", { name: "Model activity" })).toBeVisible();
|
||||||
@@ -190,8 +192,10 @@ test("administrators can explicitly switch to all sessions and see owners", asyn
|
|||||||
expect(mySessions).toHaveAttribute("tabindex", "0");
|
expect(mySessions).toHaveAttribute("tabindex", "0");
|
||||||
expect(mySessions).toHaveAttribute("data-tab-state", "active");
|
expect(mySessions).toHaveAttribute("data-tab-state", "active");
|
||||||
expect(mySessions).toHaveClass("bg-[oklch(var(--nav-active))]");
|
expect(mySessions).toHaveClass("bg-[oklch(var(--nav-active))]");
|
||||||
expect(mySessions).toHaveClass("border-b-0");
|
expect(mySessions).toHaveClass("border", "px-[11px]", "py-[3px]");
|
||||||
expect(allSessions).toHaveClass("border-b-0");
|
expect(allSessions).toHaveClass("border", "px-[11px]", "py-[3px]");
|
||||||
|
expect(mySessions).not.toHaveClass("border-b-0", "border-b-2", "-mb-px");
|
||||||
|
expect(tablist).not.toHaveClass("border-b");
|
||||||
expect(allSessions).toHaveAttribute("aria-selected", "false");
|
expect(allSessions).toHaveAttribute("aria-selected", "false");
|
||||||
expect(allSessions).toHaveAttribute("aria-controls", "session-scope-panel");
|
expect(allSessions).toHaveAttribute("aria-controls", "session-scope-panel");
|
||||||
expect(allSessions).toHaveAttribute("tabindex", "-1");
|
expect(allSessions).toHaveAttribute("tabindex", "-1");
|
||||||
@@ -403,7 +407,7 @@ test("New session closes an open session detail panel", async () => {
|
|||||||
wrap();
|
wrap();
|
||||||
await userEvent.click(await screen.findByText("Attiva uno")); // cold session → panel opens
|
await userEvent.click(await screen.findByText("Attiva uno")); // cold session → panel opens
|
||||||
await screen.findByText("Domanda originale");
|
await screen.findByText("Domanda originale");
|
||||||
await userEvent.click(screen.getByRole("button", { name: /^new session$/i }));
|
await userEvent.click(screen.getByRole("button", { name: /^Session$/i }));
|
||||||
await waitFor(() => expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByText("Domanda originale")).not.toBeInTheDocument());
|
||||||
expect(screen.getByText(/type your question/i)).toBeInTheDocument();
|
expect(screen.getByText(/type your question/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
@@ -822,7 +826,7 @@ test("starting a new question invalidates a pending Resume intent", async () =>
|
|||||||
await userEvent.click(await screen.findByText("Attiva uno"));
|
await userEvent.click(await screen.findByText("Attiva uno"));
|
||||||
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
|
await userEvent.click(await screen.findByRole("button", { name: /resume/i }));
|
||||||
await resumeStarted.promise;
|
await resumeStarted.promise;
|
||||||
await userEvent.click(screen.getByRole("button", { name: /^new session$/i }));
|
await userEvent.click(screen.getByRole("button", { name: /^Session$/i }));
|
||||||
expect(await screen.findByText(/type your question/i)).toBeInTheDocument();
|
expect(await screen.findByText(/type your question/i)).toBeInTheDocument();
|
||||||
|
|
||||||
resumeGate.resolve();
|
resumeGate.resolve();
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ interface AppShellProps {
|
|||||||
const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"];
|
const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"];
|
||||||
|
|
||||||
function sessionScopeTabClass(selected: boolean): string {
|
function sessionScopeTabClass(selected: boolean): string {
|
||||||
const base = "relative -mb-px flex h-8 w-full cursor-pointer items-center justify-center rounded-t-md border border-b-0 px-2 text-xs font-semibold tracking-[0.005em] outline-none focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-[oklch(var(--nav-active-border)/0.28)]";
|
const base = "relative flex min-h-[38px] w-full cursor-pointer items-center justify-center rounded-md border px-[11px] py-[3px] text-xs font-semibold tracking-[0.005em] outline-none focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-[oklch(var(--nav-active-border)/0.28)]";
|
||||||
return selected
|
return selected
|
||||||
? `${base} border-[oklch(var(--nav-active-border))] bg-[oklch(var(--nav-active))] text-[oklch(var(--nav-active-foreground))] hover:bg-[oklch(var(--nav-active-hover))]`
|
? `${base} border-[oklch(var(--nav-active-border))] bg-[oklch(var(--nav-active))] text-[oklch(var(--nav-active-foreground))] hover:bg-[oklch(var(--nav-active-hover))]`
|
||||||
: `${base} border-border bg-card text-muted-foreground hover:border-muted-foreground/45 hover:bg-muted/55 hover:text-foreground`;
|
: `${base} border-border bg-card text-muted-foreground hover:border-muted-foreground/45 hover:bg-muted/55 hover:text-foreground`;
|
||||||
@@ -638,6 +638,8 @@ export function AppShell({ canLogout }: AppShellProps) {
|
|||||||
deliveredToastCountRef.current = sessionToasts.length;
|
deliveredToastCountRef.current = sessionToasts.length;
|
||||||
}, [sessionToasts]);
|
}, [sessionToasts]);
|
||||||
const sessionViewOpen = Boolean(activeSessionId) || creatingSession;
|
const sessionViewOpen = Boolean(activeSessionId) || creatingSession;
|
||||||
|
const canReturnToCurrentSession = creatingSession || (Boolean(activeSessionId) && !finalized && !activeSession?.archived);
|
||||||
|
const sessionActionUnavailable = !canReturnToCurrentSession && (preprocessingBlocksNewSession || !workingContext.ready || administrationBusy);
|
||||||
const working = sessionViewOpen && !pendingWidget && agentActive;
|
const working = sessionViewOpen && !pendingWidget && agentActive;
|
||||||
// The workflow bar runs only while the harness works, not while a finalized
|
// The workflow bar runs only while the harness works, not while a finalized
|
||||||
// session sits idle or a gate awaits the reviewer (pendingWidget).
|
// session sits idle or a gate awaits the reviewer (pendingWidget).
|
||||||
@@ -965,19 +967,21 @@ export function AppShell({ canLogout }: AppShellProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="thot-session-navigation__primary-controls">
|
<div className="thot-session-navigation__primary-controls">
|
||||||
{activeSurface !== "core" && <Button variant="outline" size="sm" className="w-full" onClick={() => navigate({ surface: "core" })}>{t("Return to session")}</Button>}
|
|
||||||
<Button
|
<Button
|
||||||
variant={currentNavigation === "core" ? "navigationActive" : "outline"}
|
variant={currentNavigation === "core" ? "navigationActive" : "outline"}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="w-full min-w-0 max-w-full gap-[0.25rem] px-[0.75rem]"
|
className="w-full min-w-0 max-w-full gap-[0.25rem] px-[0.75rem]"
|
||||||
aria-current={currentNavigation === "core" ? "page" : undefined}
|
aria-current={currentNavigation === "core" ? "page" : undefined}
|
||||||
data-navigation-state={preprocessingBlocksNewSession || !workingContext.ready || administrationBusy
|
data-navigation-state={sessionActionUnavailable
|
||||||
? "unavailable"
|
? "unavailable"
|
||||||
: currentNavigation === "core" ? "current" : "available"}
|
: currentNavigation === "core" ? "current" : "available"}
|
||||||
disabled={preprocessingBlocksNewSession || !workingContext.ready || administrationBusy}
|
disabled={sessionActionUnavailable}
|
||||||
title={preprocessingBlocksNewSession ? preprocessingQuery.data?.detail : undefined}
|
title={sessionActionUnavailable && preprocessingBlocksNewSession ? preprocessingQuery.data?.detail : undefined}
|
||||||
onClick={startNewSession}
|
onClick={() => {
|
||||||
>{t("New session")}</Button>
|
if (canReturnToCurrentSession) navigate({ surface: "core" });
|
||||||
|
else startNewSession();
|
||||||
|
}}
|
||||||
|
>{t("Session")}</Button>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<Accordion.Root
|
<Accordion.Root
|
||||||
value={adminNavigationValue}
|
value={adminNavigationValue}
|
||||||
@@ -1075,7 +1079,7 @@ export function AppShell({ canLogout }: AppShellProps) {
|
|||||||
role="tablist"
|
role="tablist"
|
||||||
aria-label={t("Session scope")}
|
aria-label={t("Session scope")}
|
||||||
aria-orientation="horizontal"
|
aria-orientation="horizontal"
|
||||||
className="grid grid-cols-2 border-b border-border"
|
className="grid grid-cols-2 gap-1"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
ref={(node) => { sessionScopeTabRefs.current.mine = node; }}
|
ref={(node) => { sessionScopeTabRefs.current.mine = node; }}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function NavSessions({
|
|||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p className="px-1 py-6 text-center text-xs text-muted-foreground">
|
<p className="px-1 py-6 text-center text-xs text-muted-foreground">
|
||||||
{translate("No sessions yet.")} <span className="font-medium text-foreground">{translate("Start with New session.")}</span>
|
{translate("No sessions yet.")} <span className="font-medium text-foreground">{translate("Start with Session.")}</span>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ test("full controls change locale/theme and follow fullscreen entry, exit, Esc,
|
|||||||
expect(within(header).getByRole("button", { name: "Analyst" })).toBeInTheDocument();
|
expect(within(header).getByRole("button", { name: "Analyst" })).toBeInTheDocument();
|
||||||
expect(within(header).queryByRole("button", { name: /admin|settings/i })).not.toBeInTheDocument();
|
expect(within(header).queryByRole("button", { name: /admin|settings/i })).not.toBeInTheDocument();
|
||||||
await userEvent.selectOptions(within(header).getByRole("combobox", { name: "Interface language" }), "it");
|
await userEvent.selectOptions(within(header).getByRole("combobox", { name: "Interface language" }), "it");
|
||||||
expect(await screen.findByRole("button", { name: "Nuova sessione" })).toBeInTheDocument();
|
expect(await screen.findByRole("button", { name: "Sessione" })).toBeInTheDocument();
|
||||||
await userEvent.selectOptions(within(header).getByRole("combobox", { name: "Lingua dell’interfaccia" }), "en");
|
await userEvent.selectOptions(within(header).getByRole("combobox", { name: "Lingua dell’interfaccia" }), "en");
|
||||||
await userEvent.click(within(header).getByRole("button", { name: "Use dark theme" }));
|
await userEvent.click(within(header).getByRole("button", { name: "Use dark theme" }));
|
||||||
expect(document.documentElement).toHaveAttribute("data-bs-theme", "dark");
|
expect(document.documentElement).toHaveAttribute("data-bs-theme", "dark");
|
||||||
@@ -130,7 +130,7 @@ test("legacy embedded consumes the portal state and supplies no duplicate contro
|
|||||||
select.dataset.lang = "it";
|
select.dataset.lang = "it";
|
||||||
document.body.append(select);
|
document.body.append(select);
|
||||||
render(<App />);
|
render(<App />);
|
||||||
expect(await screen.findByRole("button", { name: "Nuova sessione" })).toBeInTheDocument();
|
expect(await screen.findByRole("button", { name: "Sessione" })).toBeInTheDocument();
|
||||||
expect(screen.queryByRole("banner", { name: /application|intestazione/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole("banner", { name: /application|intestazione/i })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("button", { name: /fullscreen|tema|Log out/i })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: /fullscreen|tema|Log out/i })).not.toBeInTheDocument();
|
||||||
act(() => { document.documentElement.setAttribute("data-bs-theme", "light"); });
|
act(() => { document.documentElement.setAttribute("data-bs-theme", "light"); });
|
||||||
@@ -160,8 +160,8 @@ test("reload restores only an authorized session document panel without resuming
|
|||||||
expect(resume).not.toHaveBeenCalled();
|
expect(resume).not.toHaveBeenCalled();
|
||||||
expect(FakeEventSource.instances).toHaveLength(0);
|
expect(FakeEventSource.instances).toHaveLength(0);
|
||||||
expect(readRememberedSession(user)).toBe("saved");
|
expect(readRememberedSession(user)).toBe("saved");
|
||||||
await waitFor(() => expect(screen.getByRole("button", { name: "New session" })).toBeEnabled());
|
await waitFor(() => expect(screen.getByRole("button", { name: "Session" })).toBeEnabled());
|
||||||
await userEvent.click(screen.getByRole("button", { name: "New session" }));
|
await userEvent.click(screen.getByRole("button", { name: "Session" }));
|
||||||
expect(readRememberedSession(user)).toBeNull();
|
expect(readRememberedSession(user)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,7 +172,7 @@ test("reload ignores a session absent from the authorized list and isolates prin
|
|||||||
expect(readRememberedSession(user)).toBeNull();
|
expect(readRememberedSession(user)).toBeNull();
|
||||||
window.history.replaceState(null, "", "/");
|
window.history.replaceState(null, "", "/");
|
||||||
render(<App />);
|
render(<App />);
|
||||||
await screen.findByRole("button", { name: "New session" });
|
await screen.findByRole("button", { name: "Session" });
|
||||||
await waitFor(() => expect(readRememberedSession(user)).toBeNull());
|
await waitFor(() => expect(readRememberedSession(user)).toBeNull());
|
||||||
expect(screen.queryByRole("button", { name: "Resume" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Resume" })).not.toBeInTheDocument();
|
||||||
expect(FakeEventSource.instances).toHaveLength(0);
|
expect(FakeEventSource.instances).toHaveLength(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user