merge(app-tauri,frontend): fix #359 — la fenêtre Tâches planifiées s'affiche en mode détaché (QA verte)
This commit is contained in:
@ -4440,9 +4440,9 @@ pub struct FocusedProjectChangedEventDto {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum ViewPanel {
|
pub(crate) enum ViewPanel {
|
||||||
Projects,
|
|
||||||
Context,
|
Context,
|
||||||
Work,
|
Work,
|
||||||
|
ScheduledTasks,
|
||||||
Tickets,
|
Tickets,
|
||||||
Mcp,
|
Mcp,
|
||||||
Agents,
|
Agents,
|
||||||
@ -4456,9 +4456,9 @@ pub(crate) enum ViewPanel {
|
|||||||
impl ViewPanel {
|
impl ViewPanel {
|
||||||
pub(crate) fn parse(raw: &str) -> Result<Self, ErrorDto> {
|
pub(crate) fn parse(raw: &str) -> Result<Self, ErrorDto> {
|
||||||
match raw {
|
match raw {
|
||||||
"projects" => Ok(Self::Projects),
|
|
||||||
"context" => Ok(Self::Context),
|
"context" => Ok(Self::Context),
|
||||||
"work" => Ok(Self::Work),
|
"work" => Ok(Self::Work),
|
||||||
|
"scheduledTasks" => Ok(Self::ScheduledTasks),
|
||||||
"tickets" => Ok(Self::Tickets),
|
"tickets" => Ok(Self::Tickets),
|
||||||
"mcp" => Ok(Self::Mcp),
|
"mcp" => Ok(Self::Mcp),
|
||||||
"agents" => Ok(Self::Agents),
|
"agents" => Ok(Self::Agents),
|
||||||
@ -4476,9 +4476,9 @@ impl ViewPanel {
|
|||||||
|
|
||||||
pub(crate) const fn as_str(self) -> &'static str {
|
pub(crate) const fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Projects => "projects",
|
|
||||||
Self::Context => "context",
|
Self::Context => "context",
|
||||||
Self::Work => "work",
|
Self::Work => "work",
|
||||||
|
Self::ScheduledTasks => "scheduledTasks",
|
||||||
Self::Tickets => "tickets",
|
Self::Tickets => "tickets",
|
||||||
Self::Mcp => "mcp",
|
Self::Mcp => "mcp",
|
||||||
Self::Agents => "agents",
|
Self::Agents => "agents",
|
||||||
@ -4492,9 +4492,9 @@ impl ViewPanel {
|
|||||||
|
|
||||||
pub(crate) const fn title(self) -> &'static str {
|
pub(crate) const fn title(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Projects => "Projects",
|
|
||||||
Self::Context => "Project context",
|
Self::Context => "Project context",
|
||||||
Self::Work => "Work state",
|
Self::Work => "Work state",
|
||||||
|
Self::ScheduledTasks => "Scheduled tasks",
|
||||||
Self::Tickets => "Tickets",
|
Self::Tickets => "Tickets",
|
||||||
Self::Mcp => "MCP",
|
Self::Mcp => "MCP",
|
||||||
Self::Agents => "Agents",
|
Self::Agents => "Agents",
|
||||||
@ -4814,6 +4814,25 @@ mod view_window_tests {
|
|||||||
assert_eq!(view_panel_from_window_label("view-mcp"), Some(panel));
|
assert_eq!(view_panel_from_window_label("view-mcp"), Some(panel));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scheduled_tasks_view_window_contract_is_stable() {
|
||||||
|
let panel = ViewPanel::parse("scheduledTasks").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(panel, ViewPanel::ScheduledTasks);
|
||||||
|
assert_eq!(panel.as_str(), "scheduledTasks");
|
||||||
|
assert_eq!(panel.title(), "Scheduled tasks");
|
||||||
|
assert_eq!(view_window_label(panel), "view-scheduledTasks");
|
||||||
|
assert_eq!(view_window_url(panel), "index.html?panel=scheduledTasks");
|
||||||
|
assert_eq!(
|
||||||
|
view_panel_from_window_label("view-scheduledTasks"),
|
||||||
|
Some(panel)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
view_panel_from_window_label("view-scheduledTasks-0000000000000000000000000000002a"),
|
||||||
|
Some(panel)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn view_window_rejects_unknown_panel() {
|
fn view_window_rejects_unknown_panel() {
|
||||||
let err = ViewPanel::parse("terminal").unwrap_err();
|
let err = ViewPanel::parse("terminal").unwrap_err();
|
||||||
@ -4822,6 +4841,15 @@ mod view_window_tests {
|
|||||||
assert!(err.message.contains("invalid view panel: terminal"));
|
assert!(err.message.contains("invalid view panel: terminal"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn projects_panel_is_not_detachable() {
|
||||||
|
let err = ViewPanel::parse("projects").unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(err.code, "INVALID");
|
||||||
|
assert!(err.message.contains("invalid view panel: projects"));
|
||||||
|
assert!(view_panel_from_window_label("view-projects").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn view_window_label_parser_accepts_stable_and_legacy_labels() {
|
fn view_window_label_parser_accepts_stable_and_legacy_labels() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@ -1132,6 +1132,26 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn persisted_identity_accepts_scheduled_tasks_view_labels() {
|
||||||
|
let (kind, panel, project_id, plugin_layout, url) =
|
||||||
|
persisted_window_identity_from_label("view-scheduledTasks").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(kind, PersistedWindowKind::View);
|
||||||
|
assert_eq!(panel.as_deref(), Some("scheduledTasks"));
|
||||||
|
assert!(project_id.is_none());
|
||||||
|
assert!(plugin_layout.is_none());
|
||||||
|
assert_eq!(url.as_deref(), Some("index.html?panel=scheduledTasks"));
|
||||||
|
assert_eq!(
|
||||||
|
persisted_view_identity_from_label(
|
||||||
|
"view-scheduledTasks-0000000000000000000000000000002a"
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.as_str(),
|
||||||
|
"scheduledTasks"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn persisted_identity_accepts_plugin_layout_view_labels() {
|
fn persisted_identity_accepts_plugin_layout_view_labels() {
|
||||||
let plugin_id = domain::PluginId::new("dev.idea.android-plugin").unwrap();
|
let plugin_id = domain::PluginId::new("dev.idea.android-plugin").unwrap();
|
||||||
@ -1157,6 +1177,7 @@ mod tests {
|
|||||||
fn persisted_identity_filters_unknown_or_headless_labels() {
|
fn persisted_identity_filters_unknown_or_headless_labels() {
|
||||||
assert!(persisted_window_identity_from_label("mcp-server").is_none());
|
assert!(persisted_window_identity_from_label("mcp-server").is_none());
|
||||||
assert!(persisted_window_identity_from_label("settings").is_none());
|
assert!(persisted_window_identity_from_label("settings").is_none());
|
||||||
|
assert!(persisted_window_identity_from_label("view-projects").is_none());
|
||||||
assert!(persisted_window_identity_from_label(
|
assert!(persisted_window_identity_from_label(
|
||||||
"view-unknown-0000000000000000000000000000002a"
|
"view-unknown-0000000000000000000000000000002a"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -41,6 +41,14 @@ describe("TauriWindowGateway (#23)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes scheduledTasks unchanged to the detached-window command", async () => {
|
||||||
|
await new TauriWindowGateway().openViewWindow("scheduledTasks");
|
||||||
|
|
||||||
|
expect(invoke).toHaveBeenCalledWith("open_view_window", {
|
||||||
|
panel: "scheduledTasks",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("opens plugin layout windows through the backend command", async () => {
|
it("opens plugin layout windows through the backend command", async () => {
|
||||||
invoke.mockResolvedValueOnce({
|
invoke.mockResolvedValueOnce({
|
||||||
label: "view-plugin-layout",
|
label: "view-plugin-layout",
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { render, screen, waitFor, act, fireEvent } from "@testing-library/react"
|
|||||||
|
|
||||||
import { DIProvider } from "@/app/di";
|
import { DIProvider } from "@/app/di";
|
||||||
import {
|
import {
|
||||||
|
createMockGateways,
|
||||||
MockAgentGateway,
|
MockAgentGateway,
|
||||||
MockFocusedProjectGateway,
|
MockFocusedProjectGateway,
|
||||||
MockProjectGateway,
|
MockProjectGateway,
|
||||||
@ -66,6 +67,13 @@ describe("parseViewWindowParams (#47)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("accepts the detachable scheduled tasks panel", () => {
|
||||||
|
expect(parseViewWindowParams("?panel=scheduledTasks")).toEqual({
|
||||||
|
kind: "panel",
|
||||||
|
panel: "scheduledTasks",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("parses a plugin layout window query from the backend base64url contract", () => {
|
it("parses a plugin layout window query from the backend base64url contract", () => {
|
||||||
expect(
|
expect(
|
||||||
parseViewWindowParams(
|
parseViewWindowParams(
|
||||||
@ -170,6 +178,29 @@ describe("PluginLayoutWindow (#143)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("ViewWindow (#47)", () => {
|
describe("ViewWindow (#47)", () => {
|
||||||
|
it("renders ScheduledTasksPanel for a focused project", async () => {
|
||||||
|
const gateways = createMockGateways();
|
||||||
|
const project = await gateways.project.createProject("alpha", "/p/a");
|
||||||
|
const focusedProject = new MockFocusedProjectGateway();
|
||||||
|
gateways.focusedProject = focusedProject;
|
||||||
|
await focusedProject.setFocusedProject({
|
||||||
|
id: project.id,
|
||||||
|
name: project.name,
|
||||||
|
root: project.root,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DIProvider gateways={gateways}>
|
||||||
|
<ViewWindow panel="scheduledTasks" />
|
||||||
|
</DIProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText("· alpha")).toBeTruthy());
|
||||||
|
expect(screen.getByRole("heading", { name: "Tâches planifiées", level: 1 })).toBeTruthy();
|
||||||
|
expect(await screen.findByRole("button", { name: "Nouvelle tâche planifiée" })).toBeTruthy();
|
||||||
|
expect(await screen.findByText("Aucune tâche planifiée pour ce projet")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the latest focused project when the initial snapshot is delayed", async () => {
|
it("keeps the latest focused project when the initial snapshot is delayed", async () => {
|
||||||
const system = new MockSystemGateway();
|
const system = new MockSystemGateway();
|
||||||
const project = new MockProjectGateway();
|
const project = new MockProjectGateway();
|
||||||
|
|||||||
@ -92,6 +92,17 @@ describe("ProjectsView detach to OS window (#23)", () => {
|
|||||||
expect(screen.queryByRole("dialog", { name: "MCP" })).toBeNull();
|
expect(screen.queryByRole("dialog", { name: "MCP" })).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("detaches scheduled tasks through WindowGateway", async () => {
|
||||||
|
const { windowGateway } = await renderWithProject();
|
||||||
|
const openViewWindow = vi.spyOn(windowGateway, "openViewWindow");
|
||||||
|
|
||||||
|
pickPlacement("Tâches planifiées", "Fenêtre détachée");
|
||||||
|
|
||||||
|
await waitFor(() => expect(openViewWindow).toHaveBeenCalledWith("scheduledTasks"));
|
||||||
|
expect(windowGateway.open.has("scheduledTasks")).toBe(true);
|
||||||
|
expect(screen.queryByRole("dialog", { name: "Tâches planifiées" })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("detaches project settings through the same retry-safe window path", async () => {
|
it("detaches project settings through the same retry-safe window path", async () => {
|
||||||
const { windowGateway } = await renderWithProject();
|
const { windowGateway } = await renderWithProject();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user