UNPKG

testeranto

Version:

the AI powered BDD test framework for typescript projects

477 lines (476 loc) 33.6 kB
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useState, useEffect } from "react"; import { Toast, ToastContainer } from "react-bootstrap"; import { useNavigate } from "react-router-dom"; import { STANDARD_LOGS, RUNTIME_SPECIFIC_LOGS } from "../../utils/logFiles"; import { Container, Row, Col, Nav, Button, Modal } from "react-bootstrap"; import { Editor } from "@monaco-editor/react"; import { NavBar } from "./NavBar"; import { TestStatusBadge } from "../TestStatusBadge"; const FileTree = ({ data, onSelect, level = 0, selectedSourcePath }) => { const [expanded, setExpanded] = useState({}); const toggleExpand = (path) => { setExpanded((prev) => (Object.assign(Object.assign({}, prev), { [path]: !prev[path] }))); }; return (React.createElement("div", null, Object.entries(data).map(([name, node]) => { const path = Object.keys(expanded).find((k) => k.endsWith(name)) || name; const isExpanded = expanded[path]; if (node.__isFile) { return (React.createElement(FileTreeItem, { key: name, name: name, isFile: true, level: level, isSelected: selectedSourcePath === path, onClick: () => onSelect(path, node.content) })); } else { return (React.createElement("div", { key: name }, React.createElement("div", { className: "d-flex align-items-center py-1 text-dark", style: { paddingLeft: `${level * 16}px`, cursor: 'pointer', fontSize: '0.875rem' }, onClick: () => toggleExpand(path) }, React.createElement("i", { className: `bi ${isExpanded ? 'bi-folder2-open' : 'bi-folder'} me-1` }), React.createElement("span", null, name)), isExpanded && (React.createElement(FileTree, { data: node, onSelect: onSelect, level: level + 1, selectedSourcePath: selectedSourcePath })))); } }))); }; export const TestPageView = ({ projectName, testName, decodedTestPath, runtime, testsExist, errorCounts, logs, }) => { const navigate = useNavigate(); const [showAiderModal, setShowAiderModal] = useState(false); const [messageOption, setMessageOption] = useState('default'); const [customMessage, setCustomMessage] = useState(typeof logs['message.txt'] === 'string' ? logs['message.txt'] : 'make a script that prints hello'); const [showToast, setShowToast] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [toastVariant, setToastVariant] = useState('success'); const [ws, setWs] = useState(null); const [expandedSections, setExpandedSections] = useState({ standardLogs: true, runtimeLogs: true, sourceFiles: true }); const [isNavbarCollapsed, setIsNavbarCollapsed] = useState(false); // Update customMessage when logs change useEffect(() => { if (typeof logs['message.txt'] === 'string' && logs['message.txt'].trim()) { setCustomMessage(logs['message.txt']); } }, [logs]); // Set up WebSocket connection useEffect(() => { const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}`; const websocket = new WebSocket(wsUrl); setWs(websocket); return () => { websocket.close(); }; }, []); const [activeTab, setActiveTab] = React.useState("tests.json"); const [selectedFile, setSelectedFile] = useState(null); const [selectedSourcePath, setSelectedSourcePath] = useState(null); const [editorTheme, setEditorTheme] = useState("vs-dark"); // Determine language from file extension const getLanguage = (path) => { var _a; const ext = (_a = path.split(".").pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase(); switch (ext) { case "ts": return "typescript"; case "tsx": return "typescript"; case "js": return "javascript"; case "json": return "json"; case "md": return "markdown"; default: return "plaintext"; } }; const renderTestResults = (testData) => { return (React.createElement("div", { className: "test-results" }, testData.givens.map((given, i) => (React.createElement("div", { key: i, className: "mb-4 card" }, React.createElement("div", { className: "card-header bg-primary text-white" }, React.createElement("div", { className: "d-flex justify-content-between align-items-center" }, React.createElement("div", null, React.createElement("h4", null, "Given: ", given.name), given.features && given.features.length > 0 && (React.createElement("div", { className: "mt-1" }, React.createElement("small", null, "Features:"), React.createElement("ul", { className: "list-unstyled" }, given.features.map((feature, fi) => (React.createElement("li", { key: fi }, feature.startsWith("http") ? (React.createElement("a", { href: feature, target: "_blank", rel: "noopener noreferrer", className: "text-white" }, new URL(feature).hostname)) : (React.createElement("span", { className: "text-white" }, feature))))))))), given.artifacts && given.artifacts.length > 0 && (React.createElement("div", { className: "dropdown" }, React.createElement("button", { className: "btn btn-sm btn-light dropdown-toggle", type: "button", "data-bs-toggle": "dropdown" }, "Artifacts (", given.artifacts.length, ")"), React.createElement("ul", { className: "dropdown-menu dropdown-menu-end" }, given.artifacts.map((artifact, ai) => (React.createElement("li", { key: ai }, React.createElement("a", { className: "dropdown-item", href: `reports/${projectName}/${testName .split(".") .slice(0, -1) .join(".")}/${runtime}/${artifact}`, target: "_blank", rel: "noopener noreferrer" }, artifact.split("/").pop()))))))))), React.createElement("div", { className: "card-body" }, given.whens.map((when, j) => (React.createElement("div", { key: `w-${j}`, className: `p-3 mb-2 ${when.error ? "bg-danger text-white" : "bg-success text-white"}` }, React.createElement("div", { className: "d-flex justify-content-between align-items-start" }, React.createElement("div", null, React.createElement("div", null, React.createElement("strong", null, "When:"), " ", when.name, when.features && when.features.length > 0 && (React.createElement("div", { className: "mt-2" }, React.createElement("small", null, "Features:"), React.createElement("ul", { className: "list-unstyled" }, when.features.map((feature, fi) => (React.createElement("li", { key: fi }, feature.startsWith("http") ? (React.createElement("a", { href: feature, target: "_blank", rel: "noopener noreferrer" }, new URL(feature).hostname)) : (feature))))))), when.error && React.createElement("pre", { className: "mt-2" }, when.error))), when.artifacts && when.artifacts.length > 0 && (React.createElement("div", { className: "ms-3" }, React.createElement("strong", null, "Artifacts:"), React.createElement("ul", { className: "list-unstyled" }, when.artifacts.map((artifact, ai) => (React.createElement("li", { key: ai }, React.createElement("a", { href: `reports/${projectName}/${testName .split(".") .slice(0, -1) .join(".")}/${runtime}/${artifact}`, target: "_blank", className: "text-white", rel: "noopener noreferrer" }, artifact.split("/").pop()))))))))))), given.thens.map((then, k) => (React.createElement("div", { key: `t-${k}`, className: `p-3 mb-2 ${then.error ? "bg-danger text-white" : "bg-success text-white"}` }, React.createElement("div", { className: "d-flex justify-content-between align-items-start" }, React.createElement("div", null, React.createElement("div", null, React.createElement("strong", null, "Then:"), " ", then.name, then.features && then.features.length > 0 && (React.createElement("div", { className: "mt-2" }, React.createElement("small", null, "Features:"), React.createElement("ul", { className: "list-unstyled" }, then.features.map((feature, fi) => (React.createElement("li", { key: fi }, feature.startsWith("http") ? (React.createElement("a", { href: feature, target: "_blank", rel: "noopener noreferrer" }, new URL(feature).hostname)) : (feature))))))), then.error && React.createElement("pre", { className: "mt-2" }, then.error))), then.artifacts && then.artifacts.length > 0 && (React.createElement("div", { className: "ms-3" }, React.createElement("strong", null, "Artifacts:"), React.createElement("ul", { className: "list-unstyled" }, then.artifacts.map((artifact, ai) => (React.createElement("li", { key: ai }, React.createElement("a", { href: `reports/${projectName}/${testName .split(".") .slice(0, -1) .join(".")}/${runtime}/${artifact}`, target: "_blank", className: "text-white", rel: "noopener noreferrer" }, artifact.split("/").pop()))))))))))))))))); }; console.log("Rendering TestPageView with logs:", { logKeys: Object.keys(logs), sourceFiles: logs.source_files ? Object.keys(logs.source_files) : null, selectedFile, activeTab, }); return (React.createElement(Container, { fluid: true, className: "px-0" }, React.createElement(NavBar, { title: decodedTestPath, backLink: `/projects/${projectName}`, navItems: [ { label: "", badge: { variant: runtime === "node" ? "primary" : runtime === "web" ? "success" : "info", text: runtime, }, className: "pe-none d-flex align-items-center gap-2", }, ], rightContent: React.createElement(Button, { variant: "info", onClick: () => setShowAiderModal(true), className: "ms-2", title: "AI Assistant" }, "\uD83E\uDD16") }), React.createElement(Modal, { show: showAiderModal, onHide: () => setShowAiderModal(false), size: "lg", onShow: () => setMessageOption('default') }, React.createElement(Modal.Header, { closeButton: true }, React.createElement(Modal.Title, null, "Aider")), React.createElement(Modal.Body, null, React.createElement("div", { className: "mb-3" }, React.createElement("div", { className: "form-check" }, React.createElement("input", { className: "form-check-input", type: "radio", name: "messageOption", id: "defaultMessage", value: "default", checked: messageOption === 'default', onChange: () => setMessageOption('default') }), React.createElement("label", { className: "form-check-label", htmlFor: "defaultMessage" }, "Use default message.txt")), React.createElement("div", { className: "form-check" }, React.createElement("input", { className: "form-check-input", type: "radio", name: "messageOption", id: "customMessage", value: "custom", checked: messageOption === 'custom', onChange: () => setMessageOption('custom') }), React.createElement("label", { className: "form-check-label", htmlFor: "customMessage" }, "Use custom message")), messageOption === 'custom' && (React.createElement("div", { className: "mt-2" }, React.createElement("textarea", { className: "form-control", rows: 8, placeholder: "Enter your custom message", value: customMessage, onChange: (e) => setCustomMessage(e.target.value), style: { minHeight: '500px' } }))))), React.createElement(Modal.Footer, null, React.createElement(Button, { variant: "primary", onClick: async () => { try { const promptPath = `testeranto/reports/${projectName}/${testName .split(".") .slice(0, -1) .join(".")}/${runtime}/prompt.txt`; let command = `aider --load ${promptPath}`; if (messageOption === 'default') { const messagePath = `testeranto/reports/${projectName}/${testName .split(".") .slice(0, -1) .join(".")}/${runtime}/message.txt`; command += ` --message-file ${messagePath}`; } else { command += ` --message "${customMessage}"`; } // Send command to server via WebSocket const ws = new WebSocket(`ws://${window.location.host}`); ws.onopen = () => { ws.send(JSON.stringify({ type: 'executeCommand', command: command })); setToastMessage('Command sent to server'); setToastVariant('success'); setShowToast(true); setShowAiderModal(false); ws.close(); // Navigate to process manager page setTimeout(() => { navigate('/processes'); }, 1000); }; ws.onerror = (error) => { setToastMessage('Failed to connect to server'); setToastVariant('danger'); setShowToast(true); }; } catch (err) { console.error("WebSocket error:", err); setToastMessage('Error preparing command'); setToastVariant('danger'); setShowToast(true); } } }, "Run Aider Command"))), React.createElement(Row, { className: "g-0" }, React.createElement(Col, { sm: 3, className: "border-end", style: { height: "calc(100vh - 56px)", overflow: "auto", backgroundColor: '#f8f9fa' } }, React.createElement("div", { className: "p-2 border-bottom" }, React.createElement("small", { className: "fw-bold text-muted" }, "EXPLORER")), React.createElement("div", { className: "p-2" }, React.createElement("div", { className: "d-flex align-items-center text-muted mb-1", style: { cursor: 'pointer', fontSize: '0.875rem' }, onClick: () => setExpandedSections(prev => (Object.assign(Object.assign({}, prev), { standardLogs: !prev.standardLogs }))) }, React.createElement("i", { className: `bi bi-chevron-${expandedSections.standardLogs ? 'down' : 'right'} me-1` }), React.createElement("span", null, "Standard Logs")), expandedSections.standardLogs && (React.createElement("div", null, Object.values(STANDARD_LOGS).map((logName) => { const logContent = logs[logName]; const exists = logContent !== undefined && ((typeof logContent === "string" && logContent.trim() !== "") || (typeof logContent === "object" && Object.keys(logContent).length > 0)); return (React.createElement(FileTreeItem, { key: logName, name: logName, isFile: true, level: 1, isSelected: activeTab === logName, exists: exists, onClick: () => { if (exists) { setActiveTab(logName); setSelectedFile({ path: logName, content: typeof logContent === "string" ? logContent : JSON.stringify(logContent, null, 2), language: logName.endsWith(".json") ? "json" : "plaintext", }); } else { setActiveTab(logName); setSelectedFile({ path: logName, content: `// ${logName} not found or empty\nThis file was not generated during the test run.`, language: "plaintext", }); } } })); })))), Object.values(RUNTIME_SPECIFIC_LOGS[runtime]).length > 0 && (React.createElement("div", { className: "p-2" }, React.createElement("div", { className: "d-flex align-items-center text-muted mb-1", style: { cursor: 'pointer', fontSize: '0.875rem' }, onClick: () => setExpandedSections(prev => (Object.assign(Object.assign({}, prev), { runtimeLogs: !prev.runtimeLogs }))) }, React.createElement("i", { className: `bi bi-chevron-${expandedSections.runtimeLogs ? 'down' : 'right'} me-1` }), React.createElement("span", null, "Runtime Logs")), expandedSections.runtimeLogs && (React.createElement("div", null, Object.values(RUNTIME_SPECIFIC_LOGS[runtime]).map((logName) => { const logContent = logs[logName]; const exists = logContent !== undefined && ((typeof logContent === "string" && logContent.trim() !== "") || (typeof logContent === "object" && Object.keys(logContent).length > 0)); return (React.createElement(FileTreeItem, { key: logName, name: logName, isFile: true, level: 1, isSelected: activeTab === logName, exists: exists, onClick: () => { if (exists) { setActiveTab(logName); setSelectedFile({ path: logName, content: typeof logContent === "string" ? logContent : JSON.stringify(logContent, null, 2), language: logName.endsWith(".json") ? "json" : "plaintext", }); } else { setActiveTab(logName); setSelectedFile({ path: logName, content: `// ${logName} not found or empty\nThis file was not generated during the test run.`, language: "plaintext", }); } } })); }))))), logs.source_files && (React.createElement("div", { className: "p-2" }, React.createElement("div", { className: "d-flex align-items-center text-muted mb-1", style: { cursor: 'pointer', fontSize: '0.875rem' }, onClick: () => setExpandedSections(prev => (Object.assign(Object.assign({}, prev), { sourceFiles: !prev.sourceFiles }))) }, React.createElement("i", { className: `bi bi-chevron-${expandedSections.sourceFiles ? 'down' : 'right'} me-1` }), React.createElement("span", null, "Source Files")), expandedSections.sourceFiles && (React.createElement("div", null, React.createElement(FileTree, { data: logs.source_files, onSelect: (path, content) => { setActiveTab("source_file"); setSelectedSourcePath(path); setSelectedFile({ path, content, language: getLanguage(path), }); }, level: 1, selectedSourcePath: selectedSourcePath })))))), React.createElement(Col, { sm: 6, className: "border-end p-0", style: { height: "calc(100vh - 56px)", overflow: "hidden" } }, React.createElement(Editor, { height: "100%", path: (selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path) || "empty", defaultLanguage: (selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.language) || "plaintext", value: (selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.content) || "// Select a file to view its contents", theme: editorTheme, options: { minimap: { enabled: false }, fontSize: 14, wordWrap: "on", automaticLayout: true, readOnly: !(selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.includes("source_files")), } })), React.createElement(Col, { sm: 3, className: "p-0 border-start", style: { height: "calc(100vh - 56px)", overflow: "auto" } }, React.createElement("div", { className: "p-3" }, (selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.endsWith("tests.json")) && (React.createElement("div", { className: "test-results-preview" }, typeof selectedFile.content === "string" ? renderTestResults(JSON.parse(selectedFile.content)) : renderTestResults(selectedFile.content))), (selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.match(/\.(png|jpg|jpeg|gif|svg)$/i)) && (React.createElement("div", { className: "text-center" }, React.createElement("img", { src: selectedFile.content, alt: selectedFile.path, className: "img-fluid", style: { maxHeight: '300px' } }), React.createElement("div", { className: "mt-2" }, React.createElement("a", { href: selectedFile.content, target: "_blank", rel: "noopener noreferrer", className: "btn btn-sm btn-outline-primary" }, "Open Full Size")))), (selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.endsWith(".json")) && !selectedFile.path.endsWith("tests.json") && (React.createElement("pre", { className: "bg-light p-2 small" }, React.createElement("code", null, selectedFile.content))), (selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.path.includes("source_files")) && (React.createElement("div", null, React.createElement("div", { className: "mb-2 small text-muted" }, React.createElement("i", { className: "bi bi-file-earmark-text me-1" }), selectedFile.path.split('/').pop()), React.createElement(Button, { variant: "outline-primary", size: "sm", className: "mb-2", onClick: () => { // TODO: Add save functionality alert("Save functionality will be implemented here"); } }, "Save Changes")))))), React.createElement(ToastContainer, { position: "top-end", className: "p-3" }, React.createElement(Toast, { show: showToast, onClose: () => setShowToast(false), delay: 3000, autohide: true, bg: toastVariant }, React.createElement(Toast.Header, null, React.createElement("strong", { className: "me-auto" }, "Command Status")), React.createElement(Toast.Body, { className: "text-white" }, toastMessage))))); }; // Simple file tree item component const FileTreeItem = ({ name, isFile, level, isSelected, exists = true, onClick }) => { const displayName = name .replace(".json", "") .replace(".txt", "") .replace(".log", "") .replace(/_/g, " ") .replace(/^std/, "Standard ") .replace(/^exit/, "Exit Code") .split('/').pop(); return (React.createElement("div", { className: `d-flex align-items-center py-1 ${isSelected ? 'text-primary fw-bold' : exists ? 'text-dark' : 'text-muted'}`, style: { paddingLeft: `${level * 16}px`, cursor: exists ? 'pointer' : 'not-allowed', fontSize: '0.875rem', opacity: exists ? 1 : 0.6 }, onClick: exists ? onClick : undefined, title: exists ? undefined : "File not found or empty" }, React.createElement("i", { className: `bi ${isFile ? (exists ? 'bi-file-earmark-text' : 'bi-file-earmark') : 'bi-folder'} me-1` }), React.createElement("span", null, displayName), !exists && (React.createElement("i", { className: "bi bi-question-circle ms-1", title: "File not found or empty" })))); }; const ArtifactTree = ({ treeData, projectName, testName, runtime, onSelect, level = 0, basePath = '' }) => { const [expanded, setExpanded] = useState({}); const toggleExpand = (path) => { setExpanded(prev => (Object.assign(Object.assign({}, prev), { [path]: !prev[path] }))); }; return (React.createElement("ul", { className: "list-unstyled", style: { paddingLeft: `${level * 16}px` } }, Object.entries(treeData).map(([name, node]) => { const fullPath = basePath ? `${basePath}/${name}` : name; const isExpanded = expanded[fullPath]; if (node.__isFile) { return (React.createElement("li", { key: fullPath, className: "py-1" }, React.createElement("a", { href: `reports/${projectName}/${testName .split('.') .slice(0, -1) .join('.')}/${runtime}/${node.path}`, target: "_blank", rel: "noopener noreferrer", className: "text-decoration-none", onClick: (e) => { e.preventDefault(); onSelect(node.path); } }, React.createElement("i", { className: "bi bi-file-earmark-text me-2" }), name))); } else { return (React.createElement("li", { key: fullPath, className: "py-1" }, React.createElement("div", { className: "d-flex align-items-center" }, React.createElement("button", { className: "btn btn-link text-start p-0 text-decoration-none me-1", onClick: () => toggleExpand(fullPath) }, React.createElement("i", { className: `bi ${isExpanded ? 'bi-folder2-open' : 'bi-folder'} me-2` }), name)), isExpanded && (React.createElement(ArtifactTree, { treeData: node, projectName: projectName, testName: testName, runtime: runtime, onSelect: onSelect, level: level + 1, basePath: fullPath })))); } }))); }; const buildArtifactTree = (testData) => { var _a; const artifactPaths = new Set(); (_a = testData.givens) === null || _a === void 0 ? void 0 : _a.forEach((given) => { var _a, _b, _c; (_a = given.artifacts) === null || _a === void 0 ? void 0 : _a.forEach((artifact) => artifactPaths.add(artifact)); (_b = given.whens) === null || _b === void 0 ? void 0 : _b.forEach((when) => { var _a; return (_a = when.artifacts) === null || _a === void 0 ? void 0 : _a.forEach((artifact) => artifactPaths.add(artifact)); }); (_c = given.thens) === null || _c === void 0 ? void 0 : _c.forEach((then) => { var _a; return (_a = then.artifacts) === null || _a === void 0 ? void 0 : _a.forEach((artifact) => artifactPaths.add(artifact)); }); }); const sortedArtifacts = Array.from(artifactPaths).sort(); return sortedArtifacts.reduce((tree, artifactPath) => { const parts = artifactPath.split('/'); let currentLevel = tree; parts.forEach((part, i) => { if (!currentLevel[part]) { currentLevel[part] = i === parts.length - 1 ? { __isFile: true, path: artifactPath } : {}; } currentLevel = currentLevel[part]; }); return tree; }, {}); }; const LogNavItem = ({ logName, logContent, activeTab, setActiveTab, setSelectedFile, errorCounts, decodedTestPath, testsExist, }) => { const displayName = logName .replace(".json", "") .replace(".txt", "") .replace(".log", "") .replace(/_/g, " ") .replace(/^std/, "Standard ") .replace(/^exit/, "Exit Code"); const logValue = typeof logContent === "string" ? logContent.trim() : ""; let statusIndicator = null; if (logName === STANDARD_LOGS.TYPE_ERRORS && (errorCounts.typeErrors > 0 || (logValue && logValue !== "0"))) { statusIndicator = (React.createElement("span", { className: "ms-1" }, "\u274C ", errorCounts.typeErrors || (logValue ? 1 : 0))); } else if (logName === STANDARD_LOGS.LINT_ERRORS && (errorCounts.staticErrors > 0 || (logValue && logValue !== "0"))) { statusIndicator = (React.createElement("span", { className: "ms-1" }, "\u274C ", errorCounts.staticErrors || (logValue ? 1 : 0))); } else if (logName === RUNTIME_SPECIFIC_LOGS.node.STDERR && (errorCounts.runTimeErrors > 0 || (logValue && logValue !== "0"))) { statusIndicator = (React.createElement("span", { className: "ms-1" }, "\u274C ", errorCounts.runTimeErrors || (logValue ? 1 : 0))); } else if (logName === STANDARD_LOGS.EXIT && logValue !== "0") { statusIndicator = React.createElement("span", { className: "ms-1" }, "\u26A0\uFE0F ", logValue); } else if (logName === STANDARD_LOGS.TESTS && logValue) { statusIndicator = (React.createElement("div", { className: "ms-1" }, React.createElement(TestStatusBadge, { testName: decodedTestPath, testsExist: testsExist, runTimeErrors: errorCounts.runTimeErrors, typeErrors: errorCounts.typeErrors, staticErrors: errorCounts.staticErrors, variant: "compact", className: "mt-1" }))); } return (React.createElement(Nav.Item, { key: logName }, React.createElement(Nav.Link, { eventKey: logName, active: activeTab === logName, onClick: () => { setActiveTab(logName); setSelectedFile({ path: logName, content: typeof logContent === "string" ? logContent : JSON.stringify(logContent, null, 2), language: logName.endsWith(".json") ? "json" : logName.endsWith(".txt") ? "plaintext" : logName.endsWith(".log") ? "log" : "plaintext", }); }, className: "d-flex flex-column align-items-start" }, React.createElement("div", { className: "d-flex justify-content-between w-100" }, React.createElement("span", { className: "text-capitalize" }, displayName), statusIndicator)))); };