🚀 Auto-deploy: BrainWind atualizado em 15/07/2026 11:22:37
This commit is contained in:
+50
-27
@@ -14,8 +14,15 @@ import PiperackModule from './pages/PiperackModule';
|
|||||||
import {
|
import {
|
||||||
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
Home, Settings, Menu, Cylinder, Church, CircleDot,
|
||||||
Square, Layers, BarChart3, Activity, Warehouse,
|
Square, Layers, BarChart3, Activity, Warehouse,
|
||||||
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen
|
Settings2, Sun, Moon, Frame, FolderOpen, BookOpen, MoreVertical, Info
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -220,16 +227,48 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
>
|
>
|
||||||
{effectiveTheme === 'dark' ? <Sun className="h-4.5 w-4.5" /> : <Moon className="h-4.5 w-4.5" />}
|
{effectiveTheme === 'dark' ? <Sun className="h-4.5 w-4.5" /> : <Moon className="h-4.5 w-4.5" />}
|
||||||
</Button>
|
</Button>
|
||||||
<Link
|
<DropdownMenu>
|
||||||
to="/settings"
|
<DropdownMenuTrigger asChild>
|
||||||
className={cn(
|
<Button
|
||||||
'h-8 w-8 flex items-center justify-center text-muted-foreground hover:text-foreground shrink-0 rounded-full border border-border/40 bg-background/50',
|
variant="ghost"
|
||||||
location.pathname === '/settings' && 'text-primary border-primary/20 bg-primary/5'
|
size="icon"
|
||||||
)}
|
className={cn(
|
||||||
title={t('nav_settings')}
|
'h-8 w-8 text-muted-foreground hover:text-foreground shrink-0 rounded-full border border-border/40 bg-background/50',
|
||||||
>
|
(location.pathname === '/settings' || location.pathname === '/arquivos') && 'text-primary border-primary/20 bg-primary/5'
|
||||||
<Settings className="h-4.5 w-4.5" />
|
)}
|
||||||
</Link>
|
>
|
||||||
|
<MoreVertical className="h-4.5 w-4.5" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-56">
|
||||||
|
<GlobalWindSettingsModal trigger={
|
||||||
|
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} className="cursor-pointer gap-2">
|
||||||
|
<Settings2 className="w-4 h-4" /> Parâmetros do Vento
|
||||||
|
</DropdownMenuItem>
|
||||||
|
} />
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/settings?tab=gerenciador" className="cursor-pointer gap-2">
|
||||||
|
<FolderOpen className="w-4 h-4" /> Arquivos
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/settings" className="cursor-pointer gap-2">
|
||||||
|
<Settings className="w-4 h-4" /> Configurações
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Link to="/settings?tab=glossary" className="cursor-pointer gap-2">
|
||||||
|
<BookOpen className="w-4 h-4" /> Glossário
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<TermsOfUseDialog trigger={
|
||||||
|
<DropdownMenuItem onSelect={(e: Event) => e.preventDefault()} className="cursor-pointer gap-2">
|
||||||
|
<Info className="w-4 h-4" /> Termos de Uso
|
||||||
|
</DropdownMenuItem>
|
||||||
|
} />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="flex-1 overflow-auto">{children}</div>
|
<div className="flex-1 overflow-auto">{children}</div>
|
||||||
@@ -280,22 +319,6 @@ function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Parâmetros do Vento (Fixo na direita) */}
|
|
||||||
<GlobalWindSettingsModal
|
|
||||||
trigger={
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex flex-col items-center justify-center h-full px-3 text-xs transition-colors shrink-0 border-l bg-card z-20 shadow-[-4px_0_8px_-4px_rgba(0,0,0,0.15)] cursor-pointer text-muted-foreground hover:text-primary',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="p-1 rounded-full">
|
|
||||||
<Settings2 className="w-5 h-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<span className="scale-90 text-[10px] text-primary font-medium">Vento</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,24 +6,27 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
interface TermsOfUseDialogProps {
|
interface TermsOfUseDialogProps {
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TermsOfUseDialog({ isCollapsed = false }: TermsOfUseDialogProps) {
|
export function TermsOfUseDialog({ isCollapsed = false, trigger }: TermsOfUseDialogProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<button
|
{trigger ? trigger : (
|
||||||
className={cn(
|
<button
|
||||||
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
className={cn(
|
||||||
isCollapsed && 'justify-center px-0'
|
'flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors text-sm w-full cursor-pointer text-muted-foreground hover:bg-secondary/50 hover:text-secondary-foreground',
|
||||||
)}
|
isCollapsed && 'justify-center px-0'
|
||||||
title={isCollapsed ? 'Termos de Uso e Responsabilidade' : undefined}
|
)}
|
||||||
>
|
title={isCollapsed ? 'Termos de Uso e Responsabilidade' : undefined}
|
||||||
<Info className="w-5 h-5 shrink-0" />
|
>
|
||||||
{!isCollapsed && <span>Termos de Uso</span>}
|
<Info className="w-5 h-5 shrink-0" />
|
||||||
</button>
|
{!isCollapsed && <span>Termos de Uso</span>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent className="max-w-2xl max-h-[85vh] p-0 flex flex-col">
|
<DialogContent className="max-w-2xl max-h-[85vh] p-0 flex flex-col">
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function DropdownMenu({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||||
|
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
data-slot="dropdown-menu-trigger"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
data-slot="dropdown-menu-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuItem({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
variant = "default",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
variant?: "default" | "destructive"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
data-slot="dropdown-menu-item"
|
||||||
|
data-inset={inset}
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuCheckboxItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
checked,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioGroup
|
||||||
|
data-slot="dropdown-menu-radio-group"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuRadioItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
data-slot="dropdown-menu-radio-item"
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CircleIcon className="size-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuLabel({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
data-slot="dropdown-menu-label"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
data-slot="dropdown-menu-separator"
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="dropdown-menu-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSub({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||||
|
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubTrigger({
|
||||||
|
className,
|
||||||
|
inset,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
data-slot="dropdown-menu-sub-trigger"
|
||||||
|
data-inset={inset}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto size-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DropdownMenuSubContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
data-slot="dropdown-menu-sub-content"
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ describe('M9.7 — parseProjectJson', () => {
|
|||||||
|
|
||||||
describe('M9.7 — detectFormat', () => {
|
describe('M9.7 — detectFormat', () => {
|
||||||
it('Detecta SavedProject', () => {
|
it('Detecta SavedProject', () => {
|
||||||
expect(detectFormat({ module: 'galpao', inputs: {} })).toBe('saved-project');
|
expect(detectFormat({ type: 'projeto', module: 'galpao', inputs: {} })).toBe('saved-project');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Detecta snapshot do windStore', () => {
|
it('Detecta snapshot do windStore', () => {
|
||||||
@@ -106,7 +106,7 @@ describe('M9.7 — validateSavedProject', () => {
|
|||||||
it('Aceita SavedProject válido', () => {
|
it('Aceita SavedProject válido', () => {
|
||||||
const project = {
|
const project = {
|
||||||
name: 'Galpão Teste',
|
name: 'Galpão Teste',
|
||||||
module: 'galpao',
|
type: 'projeto', module: 'galpao',
|
||||||
inputs: {},
|
inputs: {},
|
||||||
createdAt: 1000,
|
createdAt: 1000,
|
||||||
updatedAt: 2000,
|
updatedAt: 2000,
|
||||||
@@ -117,28 +117,28 @@ describe('M9.7 — validateSavedProject', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Rejeita projeto sem name', () => {
|
it('Rejeita projeto sem name', () => {
|
||||||
const project = { module: 'galpao', inputs: {} };
|
const project = { type: 'projeto', module: 'galpao', inputs: {} };
|
||||||
const v = validateSavedProject(project);
|
const v = validateSavedProject(project);
|
||||||
expect(v.ok).toBe(false);
|
expect(v.ok).toBe(false);
|
||||||
expect(v.errors.some((e) => e.includes('name'))).toBe(true);
|
expect(v.errors.some((e) => e.includes('name'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Rejeita módulo inválido', () => {
|
it('Rejeita módulo inválido', () => {
|
||||||
const project = { name: 'X', module: 'invalido', inputs: {} };
|
const project = { name: 'X', type: 'projeto', module: 'invalido', inputs: {} };
|
||||||
const v = validateSavedProject(project);
|
const v = validateSavedProject(project);
|
||||||
expect(v.ok).toBe(false);
|
expect(v.ok).toBe(false);
|
||||||
expect(v.errors.some((e) => e.includes('module'))).toBe(true);
|
expect(v.errors.some((e) => e.includes('module'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Rejeita inputs não-objeto', () => {
|
it('Rejeita inputs não-objeto', () => {
|
||||||
const project = { name: 'X', module: 'galpao', inputs: 'não-objeto' };
|
const project = { name: 'X', type: 'projeto', module: 'galpao', inputs: 'não-objeto' };
|
||||||
const v = validateSavedProject(project);
|
const v = validateSavedProject(project);
|
||||||
expect(v.ok).toBe(false);
|
expect(v.ok).toBe(false);
|
||||||
expect(v.errors.some((e) => e.includes('inputs'))).toBe(true);
|
expect(v.errors.some((e) => e.includes('inputs'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Emite warning se timestamps faltarem', () => {
|
it('Emite warning se timestamps faltarem', () => {
|
||||||
const project = { name: 'X', module: 'galpao', inputs: {} };
|
const project = { name: 'X', type: 'projeto', module: 'galpao', inputs: {} };
|
||||||
const v = validateSavedProject(project);
|
const v = validateSavedProject(project);
|
||||||
expect(v.warnings.length).toBeGreaterThan(0);
|
expect(v.warnings.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
@@ -177,7 +177,7 @@ describe('M9.7 — applySavedProject', () => {
|
|||||||
it('Aplica wind.v0 corretamente', () => {
|
it('Aplica wind.v0 corretamente', () => {
|
||||||
const project: SavedProject = {
|
const project: SavedProject = {
|
||||||
name: 'Teste',
|
name: 'Teste',
|
||||||
module: 'galpao',
|
type: 'projeto', module: 'galpao',
|
||||||
inputs: { wind: { v0: 50 } },
|
inputs: { wind: { v0: 50 } },
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
@@ -190,7 +190,7 @@ describe('M9.7 — applySavedProject', () => {
|
|||||||
it('Aplica múltiplos campos do windStore', () => {
|
it('Aplica múltiplos campos do windStore', () => {
|
||||||
const project: SavedProject = {
|
const project: SavedProject = {
|
||||||
name: 'Teste',
|
name: 'Teste',
|
||||||
module: 'galpao',
|
type: 'projeto', module: 'galpao',
|
||||||
inputs: {
|
inputs: {
|
||||||
wind: {
|
wind: {
|
||||||
v0: 45,
|
v0: 45,
|
||||||
@@ -215,7 +215,7 @@ describe('M9.7 — applySavedProject', () => {
|
|||||||
it('Aplica galpaoStore quando module=galpao', () => {
|
it('Aplica galpaoStore quando module=galpao', () => {
|
||||||
const project: SavedProject = {
|
const project: SavedProject = {
|
||||||
name: 'Galpão',
|
name: 'Galpão',
|
||||||
module: 'galpao',
|
type: 'projeto', module: 'galpao',
|
||||||
inputs: {
|
inputs: {
|
||||||
galpao: {
|
galpao: {
|
||||||
width: 20,
|
width: 20,
|
||||||
@@ -243,7 +243,7 @@ describe('M9.7 — applySavedProject', () => {
|
|||||||
it('Não aplica galpaoStore quando module ≠ galpao', () => {
|
it('Não aplica galpaoStore quando module ≠ galpao', () => {
|
||||||
const project: SavedProject = {
|
const project: SavedProject = {
|
||||||
name: 'Cilindro',
|
name: 'Cilindro',
|
||||||
module: 'cilindro',
|
type: 'projeto', module: 'cilindro',
|
||||||
inputs: { galpao: { width: 20 } },
|
inputs: { galpao: { width: 20 } },
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
@@ -255,7 +255,7 @@ describe('M9.7 — applySavedProject', () => {
|
|||||||
it('Adiciona warning para categoria inválida', () => {
|
it('Adiciona warning para categoria inválida', () => {
|
||||||
const project: SavedProject = {
|
const project: SavedProject = {
|
||||||
name: 'Teste',
|
name: 'Teste',
|
||||||
module: 'galpao',
|
type: 'projeto', module: 'galpao',
|
||||||
inputs: { wind: { terrainCategory: 'INVALID' } },
|
inputs: { wind: { terrainCategory: 'INVALID' } },
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
@@ -281,7 +281,7 @@ describe('M9.7 — importProjectFromText (orquestrador)', () => {
|
|||||||
it('Roundtrip: export → import preserva campos principais', () => {
|
it('Roundtrip: export → import preserva campos principais', () => {
|
||||||
const project: SavedProject = {
|
const project: SavedProject = {
|
||||||
name: 'Roundtrip',
|
name: 'Roundtrip',
|
||||||
module: 'galpao',
|
type: 'projeto', module: 'galpao',
|
||||||
inputs: {
|
inputs: {
|
||||||
wind: { v0: 45, s1: 1, s3Group: 2 },
|
wind: { v0: 45, s1: 1, s3Group: 2 },
|
||||||
galpao: { width: 18, length: 35, height: 7 },
|
galpao: { width: 18, length: 35, height: 7 },
|
||||||
|
|||||||
Reference in New Issue
Block a user