@memberjunction/ng-core-entity-forms 6.1.4 → 6.2.0-edge.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/dist/lib/custom/AIAgentCategories/ai-agent-category-overview.panel.d.ts +1 -1
- package/dist/lib/custom/AIAgentCategories/ai-agent-category-overview.panel.js +2 -2
- package/dist/lib/custom/AIAgentCategories/ai-agent-category-overview.panel.js.map +1 -1
- package/dist/lib/custom/AIAgentSessions/ai-agent-session-form.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/add-action-dialog.component.d.ts +90 -16
- package/dist/lib/custom/AIAgents/add-action-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/add-action-dialog.component.js +218 -70
- package/dist/lib/custom/AIAgents/add-action-dialog.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/agent-prompt-advanced-settings-dialog.component.d.ts +80 -16
- package/dist/lib/custom/AIAgents/agent-prompt-advanced-settings-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/agent-prompt-advanced-settings-dialog.component.js +146 -46
- package/dist/lib/custom/AIAgents/agent-prompt-advanced-settings-dialog.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/ai-agent-form.component.d.ts +329 -27
- package/dist/lib/custom/AIAgents/ai-agent-form.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/ai-agent-form.component.js +794 -262
- package/dist/lib/custom/AIAgents/ai-agent-form.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/ai-agent-management.service.d.ts +76 -0
- package/dist/lib/custom/AIAgents/ai-agent-management.service.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/ai-agent-management.service.js +77 -33
- package/dist/lib/custom/AIAgents/ai-agent-management.service.js.map +1 -1
- package/dist/lib/custom/AIAgents/create-prompt-dialog.component.d.ts +69 -15
- package/dist/lib/custom/AIAgents/create-prompt-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/create-prompt-dialog.component.js +182 -74
- package/dist/lib/custom/AIAgents/create-prompt-dialog.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/create-sub-agent-dialog.component.d.ts +105 -25
- package/dist/lib/custom/AIAgents/create-sub-agent-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/create-sub-agent-dialog.component.js +272 -112
- package/dist/lib/custom/AIAgents/create-sub-agent-dialog.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/new-agent-dialog.component.d.ts +32 -6
- package/dist/lib/custom/AIAgents/new-agent-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/new-agent-dialog.component.js +71 -19
- package/dist/lib/custom/AIAgents/new-agent-dialog.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/new-agent-dialog.service.d.ts +10 -0
- package/dist/lib/custom/AIAgents/new-agent-dialog.service.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/new-agent-dialog.service.js +27 -7
- package/dist/lib/custom/AIAgents/new-agent-dialog.service.js.map +1 -1
- package/dist/lib/custom/AIAgents/prompt-selector-dialog.component.d.ts +65 -15
- package/dist/lib/custom/AIAgents/prompt-selector-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/prompt-selector-dialog.component.js +152 -52
- package/dist/lib/custom/AIAgents/prompt-selector-dialog.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/sub-agent-advanced-settings-dialog.component.d.ts +81 -15
- package/dist/lib/custom/AIAgents/sub-agent-advanced-settings-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/sub-agent-advanced-settings-dialog.component.js +145 -45
- package/dist/lib/custom/AIAgents/sub-agent-advanced-settings-dialog.component.js.map +1 -1
- package/dist/lib/custom/AIAgents/sub-agent-selector-dialog.component.d.ts +71 -17
- package/dist/lib/custom/AIAgents/sub-agent-selector-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIAgents/sub-agent-selector-dialog.component.js +175 -67
- package/dist/lib/custom/AIAgents/sub-agent-selector-dialog.component.js.map +1 -1
- package/dist/lib/custom/AIPromptRuns/ai-prompt-run-form.component.d.ts +173 -27
- package/dist/lib/custom/AIPromptRuns/ai-prompt-run-form.component.d.ts.map +1 -1
- package/dist/lib/custom/AIPromptRuns/ai-prompt-run-form.component.js +385 -93
- package/dist/lib/custom/AIPromptRuns/ai-prompt-run-form.component.js.map +1 -1
- package/dist/lib/custom/AIPromptRuns/chat-message-viewer.component.d.ts +64 -4
- package/dist/lib/custom/AIPromptRuns/chat-message-viewer.component.d.ts.map +1 -1
- package/dist/lib/custom/AIPromptRuns/chat-message-viewer.component.js +129 -29
- package/dist/lib/custom/AIPromptRuns/chat-message-viewer.component.js.map +1 -1
- package/dist/lib/custom/AIPrompts/ai-prompt-form.component.d.ts +260 -27
- package/dist/lib/custom/AIPrompts/ai-prompt-form.component.d.ts.map +1 -1
- package/dist/lib/custom/AIPrompts/ai-prompt-form.component.js +686 -238
- package/dist/lib/custom/AIPrompts/ai-prompt-form.component.js.map +1 -1
- package/dist/lib/custom/AIPrompts/ai-prompt-management.service.d.ts +10 -0
- package/dist/lib/custom/AIPrompts/ai-prompt-management.service.d.ts.map +1 -1
- package/dist/lib/custom/AIPrompts/ai-prompt-management.service.js +17 -9
- package/dist/lib/custom/AIPrompts/ai-prompt-management.service.js.map +1 -1
- package/dist/lib/custom/AIPrompts/template-selector-dialog.component.d.ts +74 -16
- package/dist/lib/custom/AIPrompts/template-selector-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/AIPrompts/template-selector-dialog.component.js +174 -58
- package/dist/lib/custom/AIPrompts/template-selector-dialog.component.js.map +1 -1
- package/dist/lib/custom/Actions/action-execution-log-form.component.d.ts +108 -14
- package/dist/lib/custom/Actions/action-execution-log-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Actions/action-execution-log-form.component.js +258 -70
- package/dist/lib/custom/Actions/action-execution-log-form.component.js.map +1 -1
- package/dist/lib/custom/Actions/action-form.component.d.ts +198 -14
- package/dist/lib/custom/Actions/action-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Actions/action-form.component.js +387 -107
- package/dist/lib/custom/Actions/action-form.component.js.map +1 -1
- package/dist/lib/custom/BridgeProviders/bridge-provider-features.d.ts +12 -6
- package/dist/lib/custom/BridgeProviders/bridge-provider-features.d.ts.map +1 -1
- package/dist/lib/custom/BridgeProviders/bridge-provider-features.js +41 -29
- package/dist/lib/custom/BridgeProviders/bridge-provider-features.js.map +1 -1
- package/dist/lib/custom/BridgeProviders/bridge-provider-form.component.js +14 -14
- package/dist/lib/custom/BridgeProviders/bridge-provider-form.component.js.map +1 -1
- package/dist/lib/custom/Companies/company-overview.panel.d.ts +1 -1
- package/dist/lib/custom/Companies/company-overview.panel.js +2 -2
- package/dist/lib/custom/Companies/company-overview.panel.js.map +1 -1
- package/dist/lib/custom/Conversations/conversation-overview.panel.d.ts +1 -1
- package/dist/lib/custom/Conversations/conversation-overview.panel.js +2 -2
- package/dist/lib/custom/Conversations/conversation-overview.panel.js.map +1 -1
- package/dist/lib/custom/Employees/employee-overview.panel.d.ts +1 -1
- package/dist/lib/custom/Employees/employee-overview.panel.js +2 -2
- package/dist/lib/custom/Employees/employee-overview.panel.js.map +1 -1
- package/dist/lib/custom/Entities/entity-form.component.d.ts +209 -28
- package/dist/lib/custom/Entities/entity-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Entities/entity-form.component.js +494 -138
- package/dist/lib/custom/Entities/entity-form.component.js.map +1 -1
- package/dist/lib/custom/EntityActions/entityaction.form.component.d.ts +5 -1
- package/dist/lib/custom/EntityActions/entityaction.form.component.d.ts.map +1 -1
- package/dist/lib/custom/EntityActions/entityaction.form.component.js +10 -2
- package/dist/lib/custom/EntityActions/entityaction.form.component.js.map +1 -1
- package/dist/lib/custom/Files/file-form.component.d.ts +1 -1
- package/dist/lib/custom/Files/file-form.component.js +2 -2
- package/dist/lib/custom/Files/file-form.component.js.map +1 -1
- package/dist/lib/custom/Lists/list-form.component.d.ts +300 -36
- package/dist/lib/custom/Lists/list-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Lists/list-form.component.js +664 -208
- package/dist/lib/custom/Lists/list-form.component.js.map +1 -1
- package/dist/lib/custom/MLModels/ml-model-form.component.d.ts +27 -0
- package/dist/lib/custom/MLModels/ml-model-form.component.d.ts.map +1 -0
- package/dist/lib/custom/MLModels/ml-model-form.component.js +193 -0
- package/dist/lib/custom/MLModels/ml-model-form.component.js.map +1 -0
- package/dist/lib/custom/MLModels/ml-model-view-models.d.ts +75 -0
- package/dist/lib/custom/MLModels/ml-model-view-models.d.ts.map +1 -0
- package/dist/lib/custom/MLModels/ml-model-view-models.js +244 -0
- package/dist/lib/custom/MLModels/ml-model-view-models.js.map +1 -0
- package/dist/lib/custom/MLModels/ps-model-detail.component.d.ts +173 -0
- package/dist/lib/custom/MLModels/ps-model-detail.component.d.ts.map +1 -0
- package/dist/lib/custom/MLModels/ps-model-detail.component.js +883 -0
- package/dist/lib/custom/MLModels/ps-model-detail.component.js.map +1 -0
- package/dist/lib/custom/Queries/query-form.component.d.ts +201 -26
- package/dist/lib/custom/Queries/query-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Queries/query-form.component.js +429 -109
- package/dist/lib/custom/Queries/query-form.component.js.map +1 -1
- package/dist/lib/custom/Queries/query-run-dialog.component.d.ts +107 -14
- package/dist/lib/custom/Queries/query-run-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/Queries/query-run-dialog.component.js +249 -79
- package/dist/lib/custom/Queries/query-run-dialog.component.js.map +1 -1
- package/dist/lib/custom/RecordProcesses/record-process-form.component.d.ts +133 -7
- package/dist/lib/custom/RecordProcesses/record-process-form.component.d.ts.map +1 -1
- package/dist/lib/custom/RecordProcesses/record-process-form.component.js +1268 -19
- package/dist/lib/custom/RecordProcesses/record-process-form.component.js.map +1 -1
- package/dist/lib/custom/SearchScopes/searchscope-form.component.d.ts +1 -1
- package/dist/lib/custom/SearchScopes/searchscope-form.component.js +4 -4
- package/dist/lib/custom/SearchScopes/searchscope-form.component.js.map +1 -1
- package/dist/lib/custom/SessionBridges/session-bridge-form.component.js.map +1 -1
- package/dist/lib/custom/Templates/template-param-dialog.component.d.ts +77 -13
- package/dist/lib/custom/Templates/template-param-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/Templates/template-param-dialog.component.js +196 -78
- package/dist/lib/custom/Templates/template-param-dialog.component.js.map +1 -1
- package/dist/lib/custom/Templates/template-params-grid.component.d.ts +49 -7
- package/dist/lib/custom/Templates/template-params-grid.component.d.ts.map +1 -1
- package/dist/lib/custom/Templates/template-params-grid.component.js +132 -44
- package/dist/lib/custom/Templates/template-params-grid.component.js.map +1 -1
- package/dist/lib/custom/Templates/templates-form.component.d.ts +142 -17
- package/dist/lib/custom/Templates/templates-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Templates/templates-form.component.js +333 -113
- package/dist/lib/custom/Templates/templates-form.component.js.map +1 -1
- package/dist/lib/custom/Tests/entity-link-pill.component.d.ts +24 -4
- package/dist/lib/custom/Tests/entity-link-pill.component.d.ts.map +1 -1
- package/dist/lib/custom/Tests/entity-link-pill.component.js +64 -20
- package/dist/lib/custom/Tests/entity-link-pill.component.js.map +1 -1
- package/dist/lib/custom/Tests/test-form.component.d.ts +217 -27
- package/dist/lib/custom/Tests/test-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Tests/test-form.component.js +549 -169
- package/dist/lib/custom/Tests/test-form.component.js.map +1 -1
- package/dist/lib/custom/Tests/test-run-checks.d.ts +2 -0
- package/dist/lib/custom/Tests/test-run-checks.d.ts.map +1 -1
- package/dist/lib/custom/Tests/test-run-checks.js +5 -1
- package/dist/lib/custom/Tests/test-run-checks.js.map +1 -1
- package/dist/lib/custom/Tests/test-run-feedback-form.component.d.ts +2 -0
- package/dist/lib/custom/Tests/test-run-feedback-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Tests/test-run-feedback-form.component.js +5 -1
- package/dist/lib/custom/Tests/test-run-feedback-form.component.js.map +1 -1
- package/dist/lib/custom/Tests/test-run-form.component.d.ts +146 -18
- package/dist/lib/custom/Tests/test-run-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Tests/test-run-form.component.js +362 -106
- package/dist/lib/custom/Tests/test-run-form.component.js.map +1 -1
- package/dist/lib/custom/Tests/test-suite-form.component.d.ts +390 -47
- package/dist/lib/custom/Tests/test-suite-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Tests/test-suite-form.component.js +1015 -343
- package/dist/lib/custom/Tests/test-suite-form.component.js.map +1 -1
- package/dist/lib/custom/Tests/test-suite-run-form.component.d.ts +214 -26
- package/dist/lib/custom/Tests/test-suite-run-form.component.d.ts.map +1 -1
- package/dist/lib/custom/Tests/test-suite-run-form.component.js +535 -159
- package/dist/lib/custom/Tests/test-suite-run-form.component.js.map +1 -1
- package/dist/lib/custom/Users/user-overview.panel.d.ts +1 -1
- package/dist/lib/custom/Users/user-overview.panel.js +2 -2
- package/dist/lib/custom/Users/user-overview.panel.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-analytics.component.d.ts +208 -29
- package/dist/lib/custom/ai-agent-run/ai-agent-run-analytics.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-analytics.component.js +446 -138
- package/dist/lib/custom/ai-agent-run/ai-agent-run-analytics.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-cost.service.d.ts +6 -0
- package/dist/lib/custom/ai-agent-run/ai-agent-run-cost.service.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-cost.service.js +16 -4
- package/dist/lib/custom/ai-agent-run/ai-agent-run-cost.service.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-data.service.d.ts +50 -6
- package/dist/lib/custom/ai-agent-run/ai-agent-run-data.service.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-data.service.js +84 -12
- package/dist/lib/custom/ai-agent-run/ai-agent-run-data.service.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-step-detail.component.d.ts +89 -5
- package/dist/lib/custom/ai-agent-run/ai-agent-run-step-detail.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-step-detail.component.js +216 -70
- package/dist/lib/custom/ai-agent-run/ai-agent-run-step-detail.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-step-node.component.d.ts +62 -3
- package/dist/lib/custom/ai-agent-run/ai-agent-run-step-node.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-step-node.component.js +140 -42
- package/dist/lib/custom/ai-agent-run/ai-agent-run-step-node.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-timeline.component.d.ts +81 -10
- package/dist/lib/custom/ai-agent-run/ai-agent-run-timeline.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run-timeline.component.js +165 -37
- package/dist/lib/custom/ai-agent-run/ai-agent-run-timeline.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run.component.d.ts +121 -14
- package/dist/lib/custom/ai-agent-run/ai-agent-run.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/ai-agent-run.component.js +287 -79
- package/dist/lib/custom/ai-agent-run/ai-agent-run.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/agent-run-flow.component.d.ts +101 -15
- package/dist/lib/custom/ai-agent-run/flow/agent-run-flow.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/agent-run-flow.component.js +281 -97
- package/dist/lib/custom/ai-agent-run/flow/agent-run-flow.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/agent-run-flow.model.d.ts +43 -25
- package/dist/lib/custom/ai-agent-run/flow/agent-run-flow.model.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/agent-run-flow.model.js +100 -64
- package/dist/lib/custom/ai-agent-run/flow/agent-run-flow.model.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/constellation.component.d.ts +14 -2
- package/dist/lib/custom/ai-agent-run/flow/constellation.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/constellation.component.js +71 -53
- package/dist/lib/custom/ai-agent-run/flow/constellation.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/flame-cascade.component.d.ts +14 -2
- package/dist/lib/custom/ai-agent-run/flow/flame-cascade.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/flame-cascade.component.js +70 -52
- package/dist/lib/custom/ai-agent-run/flow/flame-cascade.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/flow-svg.util.d.ts +8 -0
- package/dist/lib/custom/ai-agent-run/flow/flow-svg.util.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/flow-svg.util.js +22 -6
- package/dist/lib/custom/ai-agent-run/flow/flow-svg.util.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/flowchart.component.d.ts +24 -2
- package/dist/lib/custom/ai-agent-run/flow/flowchart.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/flowchart.component.js +91 -59
- package/dist/lib/custom/ai-agent-run/flow/flowchart.component.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/pan-zoom.d.ts +10 -2
- package/dist/lib/custom/ai-agent-run/flow/pan-zoom.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/pan-zoom.js +33 -17
- package/dist/lib/custom/ai-agent-run/flow/pan-zoom.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/run-tree-flow-projection.d.ts +2 -0
- package/dist/lib/custom/ai-agent-run/flow/run-tree-flow-projection.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/run-tree-flow-projection.js +34 -30
- package/dist/lib/custom/ai-agent-run/flow/run-tree-flow-projection.js.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/subway-lines.component.d.ts +14 -2
- package/dist/lib/custom/ai-agent-run/flow/subway-lines.component.d.ts.map +1 -1
- package/dist/lib/custom/ai-agent-run/flow/subway-lines.component.js +69 -51
- package/dist/lib/custom/ai-agent-run/flow/subway-lines.component.js.map +1 -1
- package/dist/lib/custom/custom-forms.module.d.ts +32 -30
- package/dist/lib/custom/custom-forms.module.d.ts.map +1 -1
- package/dist/lib/custom/custom-forms.module.js +21 -4
- package/dist/lib/custom/custom-forms.module.js.map +1 -1
- package/dist/lib/custom/shared/entity-selector-dialog.component.d.ts +38 -12
- package/dist/lib/custom/shared/entity-selector-dialog.component.d.ts.map +1 -1
- package/dist/lib/custom/shared/entity-selector-dialog.component.js +103 -51
- package/dist/lib/custom/shared/entity-selector-dialog.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJAIPrompt/mjaiprompt.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJAIPrompt/mjaiprompt.form.component.js +62 -26
- package/dist/lib/generated/Entities/MJAIPrompt/mjaiprompt.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJAIPromptRun/mjaipromptrun.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJAIPromptRun/mjaipromptrun.form.component.js +59 -51
- package/dist/lib/generated/Entities/MJAIPromptRun/mjaipromptrun.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJAIVendor/mjaivendor.form.component.js +76 -74
- package/dist/lib/generated/Entities/MJAIVendor/mjaivendor.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJCredential/mjcredential.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJCredential/mjcredential.form.component.js +33 -15
- package/dist/lib/generated/Entities/MJCredential/mjcredential.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJEntity/mjentity.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJEntity/mjentity.form.component.js +100 -82
- package/dist/lib/generated/Entities/MJEntity/mjentity.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJEntityField/mjentityfield.form.component.js +24 -20
- package/dist/lib/generated/Entities/MJEntityField/mjentityfield.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJFeatureValue/mjfeaturevalue.form.component.d.ts +10 -0
- package/dist/lib/generated/Entities/MJFeatureValue/mjfeaturevalue.form.component.d.ts.map +1 -0
- package/dist/lib/generated/Entities/MJFeatureValue/mjfeaturevalue.form.component.js +122 -0
- package/dist/lib/generated/Entities/MJFeatureValue/mjfeaturevalue.form.component.js.map +1 -0
- package/dist/lib/generated/Entities/MJFeatureValueCache/mjfeaturevaluecache.form.component.d.ts +10 -0
- package/dist/lib/generated/Entities/MJFeatureValueCache/mjfeaturevaluecache.form.component.d.ts.map +1 -0
- package/dist/lib/generated/Entities/MJFeatureValueCache/mjfeaturevaluecache.form.component.js +125 -0
- package/dist/lib/generated/Entities/MJFeatureValueCache/mjfeaturevaluecache.form.component.js.map +1 -0
- package/dist/lib/generated/Entities/MJProcessRun/mjprocessrun.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJProcessRun/mjprocessrun.form.component.js +23 -5
- package/dist/lib/generated/Entities/MJProcessRun/mjprocessrun.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJProcessRunDetail/mjprocessrundetail.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJProcessRunDetail/mjprocessrundetail.form.component.js +25 -6
- package/dist/lib/generated/Entities/MJProcessRunDetail/mjprocessrundetail.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJProject/mjproject.form.component.js +27 -25
- package/dist/lib/generated/Entities/MJProject/mjproject.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJQuery/mjquery.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJQuery/mjquery.form.component.js +11 -15
- package/dist/lib/generated/Entities/MJQuery/mjquery.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJRecordProcess/mjrecordprocess.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJRecordProcess/mjrecordprocess.form.component.js +43 -7
- package/dist/lib/generated/Entities/MJRecordProcess/mjrecordprocess.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJUser/mjuser.form.component.d.ts.map +1 -1
- package/dist/lib/generated/Entities/MJUser/mjuser.form.component.js +155 -119
- package/dist/lib/generated/Entities/MJUser/mjuser.form.component.js.map +1 -1
- package/dist/lib/generated/Entities/MJWebSearchProvider/mjwebsearchprovider.form.component.d.ts +10 -0
- package/dist/lib/generated/Entities/MJWebSearchProvider/mjwebsearchprovider.form.component.d.ts.map +1 -0
- package/dist/lib/generated/Entities/MJWebSearchProvider/mjwebsearchprovider.form.component.js +78 -0
- package/dist/lib/generated/Entities/MJWebSearchProvider/mjwebsearchprovider.form.component.js.map +1 -0
- package/dist/lib/generated/generated-forms.module.d.ts +240 -237
- package/dist/lib/generated/generated-forms.module.d.ts.map +1 -1
- package/dist/lib/generated/generated-forms.module.js +11 -2
- package/dist/lib/generated/generated-forms.module.js.map +1 -1
- package/dist/lib/panels/ai-skill-sharing/ai-skill-sharing-panel.component.d.ts +5 -1
- package/dist/lib/panels/ai-skill-sharing/ai-skill-sharing-panel.component.d.ts.map +1 -1
- package/dist/lib/panels/ai-skill-sharing/ai-skill-sharing-panel.component.js +11 -3
- package/dist/lib/panels/ai-skill-sharing/ai-skill-sharing-panel.component.js.map +1 -1
- package/dist/lib/panels/content-sources/tag-pipeline-configuration.panel.d.ts +2 -0
- package/dist/lib/panels/content-sources/tag-pipeline-configuration.panel.d.ts.map +1 -1
- package/dist/lib/panels/content-sources/tag-pipeline-configuration.panel.js +16 -12
- package/dist/lib/panels/content-sources/tag-pipeline-configuration.panel.js.map +1 -1
- package/dist/lib/panels/content-sources/website-crawler-settings.panel.d.ts +1 -1
- package/dist/lib/panels/content-sources/website-crawler-settings.panel.js +2 -2
- package/dist/lib/panels/content-sources/website-crawler-settings.panel.js.map +1 -1
- package/dist/lib/panels/model-predictions/model-prediction.logic.d.ts +122 -1
- package/dist/lib/panels/model-predictions/model-prediction.logic.d.ts.map +1 -1
- package/dist/lib/panels/model-predictions/model-prediction.logic.js +236 -11
- package/dist/lib/panels/model-predictions/model-prediction.logic.js.map +1 -1
- package/dist/lib/panels/model-predictions/model-prediction.panel.d.ts +72 -13
- package/dist/lib/panels/model-predictions/model-prediction.panel.d.ts.map +1 -1
- package/dist/lib/panels/model-predictions/model-prediction.panel.js +577 -78
- package/dist/lib/panels/model-predictions/model-prediction.panel.js.map +1 -1
- package/dist/lib/shared/components/template-editor.component.d.ts +133 -14
- package/dist/lib/shared/components/template-editor.component.d.ts.map +1 -1
- package/dist/lib/shared/components/template-editor.component.js +322 -124
- package/dist/lib/shared/components/template-editor.component.js.map +1 -1
- package/dist/public-api.d.ts +4 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +4 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +43 -41
|
@@ -2008,78 +2008,450 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2008
2008
|
super(...arguments);
|
|
2009
2009
|
this.destroy$ = new Subject();
|
|
2010
2010
|
// UI state
|
|
2011
|
-
this.
|
|
2012
|
-
this.
|
|
2013
|
-
this.
|
|
2014
|
-
this.
|
|
2015
|
-
this.
|
|
2016
|
-
this.
|
|
2017
|
-
this.
|
|
2018
|
-
this.
|
|
2019
|
-
this.
|
|
2020
|
-
this.isRefreshing = false;
|
|
2011
|
+
this.ActiveTab = 'overview';
|
|
2012
|
+
this.Loading = false;
|
|
2013
|
+
this.LoadingTests = false;
|
|
2014
|
+
this.LoadingRuns = false;
|
|
2015
|
+
this.LoadingAnalytics = false;
|
|
2016
|
+
this.LoadingCompare = false;
|
|
2017
|
+
this.TestsLoaded = false;
|
|
2018
|
+
this.RunsLoaded = false;
|
|
2019
|
+
this.AnalyticsLoaded = false;
|
|
2020
|
+
this.isRefreshing = false; // case-violation-ok-legacy-back-compat: an ancestor class already declares the PascalCase name
|
|
2021
2021
|
this.error = null;
|
|
2022
2022
|
// Related data
|
|
2023
|
-
this.
|
|
2024
|
-
this.
|
|
2023
|
+
this.SuiteTests = [];
|
|
2024
|
+
this.SuiteRuns = [];
|
|
2025
2025
|
// Analytics data
|
|
2026
|
-
this.
|
|
2027
|
-
this.
|
|
2028
|
-
this.
|
|
2029
|
-
this.
|
|
2030
|
-
this.
|
|
2031
|
-
this.
|
|
2032
|
-
this.
|
|
2033
|
-
this.
|
|
2026
|
+
this.AnalyticsData = [];
|
|
2027
|
+
this.UniqueTags = [];
|
|
2028
|
+
this.SelectedTags = [];
|
|
2029
|
+
this.AnalyticsTimeRange = '30d';
|
|
2030
|
+
this.AnalyticsView = 'summary';
|
|
2031
|
+
this.MatrixData = [];
|
|
2032
|
+
this.LoadingMatrix = false;
|
|
2033
|
+
this.MatrixLoaded = false;
|
|
2034
2034
|
this.chartRendered = false;
|
|
2035
2035
|
// Compare data
|
|
2036
|
-
this.
|
|
2037
|
-
this.
|
|
2038
|
-
this.
|
|
2039
|
-
this.
|
|
2040
|
-
this.
|
|
2036
|
+
this.CompareRunA = null;
|
|
2037
|
+
this.CompareRunB = null;
|
|
2038
|
+
this.CompareResults = [];
|
|
2039
|
+
this.CompareRunATests = [];
|
|
2040
|
+
this.CompareRunBTests = [];
|
|
2041
2041
|
// Keyboard shortcuts
|
|
2042
|
-
this.
|
|
2043
|
-
this.
|
|
2042
|
+
this.KeyboardShortcutsEnabled = true;
|
|
2043
|
+
this.ShowShortcuts = false;
|
|
2044
2044
|
this.shortcutsSettingEntity = null;
|
|
2045
2045
|
// Evaluation preferences
|
|
2046
|
-
this.
|
|
2046
|
+
this.EvalPreferences = { showExecution: true, showHuman: true, showAuto: false };
|
|
2047
2047
|
// Filter collapse state
|
|
2048
|
-
this.
|
|
2048
|
+
this.FiltersCollapsed = false;
|
|
2049
2049
|
// Matrix sorting
|
|
2050
|
-
this.
|
|
2051
|
-
this.
|
|
2050
|
+
this.MatrixSortBy = 'sequence';
|
|
2051
|
+
this.MatrixSortAsc = true;
|
|
2052
2052
|
// Matrix row selection
|
|
2053
|
-
this.
|
|
2053
|
+
this.SelectedMatrixTestId = null;
|
|
2054
2054
|
// Matrix test name filter
|
|
2055
|
-
this.
|
|
2055
|
+
this.MatrixTestFilter = '';
|
|
2056
2056
|
this.matrixFilterSubject$ = new Subject();
|
|
2057
2057
|
// Edit state
|
|
2058
|
-
this.
|
|
2059
|
-
this.
|
|
2060
|
-
this.
|
|
2061
|
-
this.
|
|
2058
|
+
this.IsSaving = false;
|
|
2059
|
+
this.ParentSuiteOptions = [];
|
|
2060
|
+
this.TagDraft = '';
|
|
2061
|
+
this.StatusOptions = ['Active', 'Pending', 'Disabled'];
|
|
2062
2062
|
// Add Tests picker state
|
|
2063
|
-
this.
|
|
2064
|
-
this.
|
|
2065
|
-
this.
|
|
2066
|
-
this.
|
|
2067
|
-
this.
|
|
2068
|
-
this.
|
|
2063
|
+
this.ShowAddTestsDialog = false;
|
|
2064
|
+
this.AvailableTests = [];
|
|
2065
|
+
this.LoadingAvailableTests = false;
|
|
2066
|
+
this.SelectedTestIdsToAdd = new Set();
|
|
2067
|
+
this.AddTestsSearch = '';
|
|
2068
|
+
this.IsAddingTests = false;
|
|
2069
2069
|
// Remove-confirm state (per-row inline confirm)
|
|
2070
|
-
this.
|
|
2071
|
-
this.
|
|
2070
|
+
this.ConfirmingRemoveSuiteTestId = null;
|
|
2071
|
+
this.IsRemovingTest = false;
|
|
2072
2072
|
// Reorder state
|
|
2073
|
-
this.
|
|
2073
|
+
this.IsReorderingTests = false;
|
|
2074
2074
|
// Service injections
|
|
2075
2075
|
this.navigationService = inject(NavigationService);
|
|
2076
|
-
this.
|
|
2076
|
+
this.TestingDialogService = inject(TestingDialogService);
|
|
2077
2077
|
this.evalPrefsService = inject(EvaluationPreferencesService);
|
|
2078
2078
|
this.viewContainerRef = inject(ViewContainerRef);
|
|
2079
2079
|
this.appManager = inject(ApplicationManager);
|
|
2080
2080
|
this.confirmService = inject(MJConfirmService);
|
|
2081
2081
|
}
|
|
2082
|
+
/** @deprecated Use {@link ActiveTab}. */
|
|
2083
|
+
get activeTab() {
|
|
2084
|
+
return this.ActiveTab;
|
|
2085
|
+
}
|
|
2086
|
+
/** @deprecated Use {@link ActiveTab}. */
|
|
2087
|
+
set activeTab(value) {
|
|
2088
|
+
this.ActiveTab = value;
|
|
2089
|
+
}
|
|
2090
|
+
/** @deprecated Use {@link Loading}. */
|
|
2091
|
+
get loading() {
|
|
2092
|
+
return this.Loading;
|
|
2093
|
+
}
|
|
2094
|
+
/** @deprecated Use {@link Loading}. */
|
|
2095
|
+
set loading(value) {
|
|
2096
|
+
this.Loading = value;
|
|
2097
|
+
}
|
|
2098
|
+
/** @deprecated Use {@link LoadingTests}. */
|
|
2099
|
+
get loadingTests() {
|
|
2100
|
+
return this.LoadingTests;
|
|
2101
|
+
}
|
|
2102
|
+
/** @deprecated Use {@link LoadingTests}. */
|
|
2103
|
+
set loadingTests(value) {
|
|
2104
|
+
this.LoadingTests = value;
|
|
2105
|
+
}
|
|
2106
|
+
/** @deprecated Use {@link LoadingRuns}. */
|
|
2107
|
+
get loadingRuns() {
|
|
2108
|
+
return this.LoadingRuns;
|
|
2109
|
+
}
|
|
2110
|
+
/** @deprecated Use {@link LoadingRuns}. */
|
|
2111
|
+
set loadingRuns(value) {
|
|
2112
|
+
this.LoadingRuns = value;
|
|
2113
|
+
}
|
|
2114
|
+
/** @deprecated Use {@link LoadingAnalytics}. */
|
|
2115
|
+
get loadingAnalytics() {
|
|
2116
|
+
return this.LoadingAnalytics;
|
|
2117
|
+
}
|
|
2118
|
+
/** @deprecated Use {@link LoadingAnalytics}. */
|
|
2119
|
+
set loadingAnalytics(value) {
|
|
2120
|
+
this.LoadingAnalytics = value;
|
|
2121
|
+
}
|
|
2122
|
+
/** @deprecated Use {@link LoadingCompare}. */
|
|
2123
|
+
get loadingCompare() {
|
|
2124
|
+
return this.LoadingCompare;
|
|
2125
|
+
}
|
|
2126
|
+
/** @deprecated Use {@link LoadingCompare}. */
|
|
2127
|
+
set loadingCompare(value) {
|
|
2128
|
+
this.LoadingCompare = value;
|
|
2129
|
+
}
|
|
2130
|
+
/** @deprecated Use {@link TestsLoaded}. */
|
|
2131
|
+
get testsLoaded() {
|
|
2132
|
+
return this.TestsLoaded;
|
|
2133
|
+
}
|
|
2134
|
+
/** @deprecated Use {@link TestsLoaded}. */
|
|
2135
|
+
set testsLoaded(value) {
|
|
2136
|
+
this.TestsLoaded = value;
|
|
2137
|
+
}
|
|
2138
|
+
/** @deprecated Use {@link RunsLoaded}. */
|
|
2139
|
+
get runsLoaded() {
|
|
2140
|
+
return this.RunsLoaded;
|
|
2141
|
+
}
|
|
2142
|
+
/** @deprecated Use {@link RunsLoaded}. */
|
|
2143
|
+
set runsLoaded(value) {
|
|
2144
|
+
this.RunsLoaded = value;
|
|
2145
|
+
}
|
|
2146
|
+
/** @deprecated Use {@link AnalyticsLoaded}. */
|
|
2147
|
+
get analyticsLoaded() {
|
|
2148
|
+
return this.AnalyticsLoaded;
|
|
2149
|
+
}
|
|
2150
|
+
/** @deprecated Use {@link AnalyticsLoaded}. */
|
|
2151
|
+
set analyticsLoaded(value) {
|
|
2152
|
+
this.AnalyticsLoaded = value;
|
|
2153
|
+
}
|
|
2154
|
+
/** @deprecated Use {@link SuiteTests}. */
|
|
2155
|
+
get suiteTests() {
|
|
2156
|
+
return this.SuiteTests;
|
|
2157
|
+
}
|
|
2158
|
+
/** @deprecated Use {@link SuiteTests}. */
|
|
2159
|
+
set suiteTests(value) {
|
|
2160
|
+
this.SuiteTests = value;
|
|
2161
|
+
}
|
|
2162
|
+
/** @deprecated Use {@link SuiteRuns}. */
|
|
2163
|
+
get suiteRuns() {
|
|
2164
|
+
return this.SuiteRuns;
|
|
2165
|
+
}
|
|
2166
|
+
/** @deprecated Use {@link SuiteRuns}. */
|
|
2167
|
+
set suiteRuns(value) {
|
|
2168
|
+
this.SuiteRuns = value;
|
|
2169
|
+
}
|
|
2170
|
+
/** @deprecated Use {@link AnalyticsData}. */
|
|
2171
|
+
get analyticsData() {
|
|
2172
|
+
return this.AnalyticsData;
|
|
2173
|
+
}
|
|
2174
|
+
/** @deprecated Use {@link AnalyticsData}. */
|
|
2175
|
+
set analyticsData(value) {
|
|
2176
|
+
this.AnalyticsData = value;
|
|
2177
|
+
}
|
|
2178
|
+
/** @deprecated Use {@link UniqueTags}. */
|
|
2179
|
+
get uniqueTags() {
|
|
2180
|
+
return this.UniqueTags;
|
|
2181
|
+
}
|
|
2182
|
+
/** @deprecated Use {@link UniqueTags}. */
|
|
2183
|
+
set uniqueTags(value) {
|
|
2184
|
+
this.UniqueTags = value;
|
|
2185
|
+
}
|
|
2186
|
+
/** @deprecated Use {@link SelectedTags}. */
|
|
2187
|
+
get selectedTags() {
|
|
2188
|
+
return this.SelectedTags;
|
|
2189
|
+
}
|
|
2190
|
+
/** @deprecated Use {@link SelectedTags}. */
|
|
2191
|
+
set selectedTags(value) {
|
|
2192
|
+
this.SelectedTags = value;
|
|
2193
|
+
} // Multi-select: empty array means "All Tags"
|
|
2194
|
+
/** @deprecated Use {@link AnalyticsTimeRange}. */
|
|
2195
|
+
get analyticsTimeRange() {
|
|
2196
|
+
return this.AnalyticsTimeRange;
|
|
2197
|
+
}
|
|
2198
|
+
/** @deprecated Use {@link AnalyticsTimeRange}. */
|
|
2199
|
+
set analyticsTimeRange(value) {
|
|
2200
|
+
this.AnalyticsTimeRange = value;
|
|
2201
|
+
}
|
|
2202
|
+
/** @deprecated Use {@link AnalyticsView}. */
|
|
2203
|
+
get analyticsView() {
|
|
2204
|
+
return this.AnalyticsView;
|
|
2205
|
+
}
|
|
2206
|
+
/** @deprecated Use {@link AnalyticsView}. */
|
|
2207
|
+
set analyticsView(value) {
|
|
2208
|
+
this.AnalyticsView = value;
|
|
2209
|
+
}
|
|
2210
|
+
/** @deprecated Use {@link MatrixData}. */
|
|
2211
|
+
get matrixData() {
|
|
2212
|
+
return this.MatrixData;
|
|
2213
|
+
}
|
|
2214
|
+
/** @deprecated Use {@link MatrixData}. */
|
|
2215
|
+
set matrixData(value) {
|
|
2216
|
+
this.MatrixData = value;
|
|
2217
|
+
}
|
|
2218
|
+
/** @deprecated Use {@link LoadingMatrix}. */
|
|
2219
|
+
get loadingMatrix() {
|
|
2220
|
+
return this.LoadingMatrix;
|
|
2221
|
+
}
|
|
2222
|
+
/** @deprecated Use {@link LoadingMatrix}. */
|
|
2223
|
+
set loadingMatrix(value) {
|
|
2224
|
+
this.LoadingMatrix = value;
|
|
2225
|
+
}
|
|
2226
|
+
/** @deprecated Use {@link MatrixLoaded}. */
|
|
2227
|
+
get matrixLoaded() {
|
|
2228
|
+
return this.MatrixLoaded;
|
|
2229
|
+
}
|
|
2230
|
+
/** @deprecated Use {@link MatrixLoaded}. */
|
|
2231
|
+
set matrixLoaded(value) {
|
|
2232
|
+
this.MatrixLoaded = value;
|
|
2233
|
+
}
|
|
2234
|
+
/** @deprecated Use {@link ChartContainer}. */
|
|
2235
|
+
get chartContainer() {
|
|
2236
|
+
return this.ChartContainer;
|
|
2237
|
+
}
|
|
2238
|
+
/** @deprecated Use {@link ChartContainer}. */
|
|
2239
|
+
set chartContainer(value) {
|
|
2240
|
+
this.ChartContainer = value;
|
|
2241
|
+
}
|
|
2242
|
+
/** @deprecated Use {@link CompareRunA}. */
|
|
2243
|
+
get compareRunA() {
|
|
2244
|
+
return this.CompareRunA;
|
|
2245
|
+
}
|
|
2246
|
+
/** @deprecated Use {@link CompareRunA}. */
|
|
2247
|
+
set compareRunA(value) {
|
|
2248
|
+
this.CompareRunA = value;
|
|
2249
|
+
}
|
|
2250
|
+
/** @deprecated Use {@link CompareRunB}. */
|
|
2251
|
+
get compareRunB() {
|
|
2252
|
+
return this.CompareRunB;
|
|
2253
|
+
}
|
|
2254
|
+
/** @deprecated Use {@link CompareRunB}. */
|
|
2255
|
+
set compareRunB(value) {
|
|
2256
|
+
this.CompareRunB = value;
|
|
2257
|
+
}
|
|
2258
|
+
/** @deprecated Use {@link CompareResults}. */
|
|
2259
|
+
get compareResults() {
|
|
2260
|
+
return this.CompareResults;
|
|
2261
|
+
}
|
|
2262
|
+
/** @deprecated Use {@link CompareResults}. */
|
|
2263
|
+
set compareResults(value) {
|
|
2264
|
+
this.CompareResults = value;
|
|
2265
|
+
}
|
|
2266
|
+
/** @deprecated Use {@link CompareRunATests}. */
|
|
2267
|
+
get compareRunATests() {
|
|
2268
|
+
return this.CompareRunATests;
|
|
2269
|
+
}
|
|
2270
|
+
/** @deprecated Use {@link CompareRunATests}. */
|
|
2271
|
+
set compareRunATests(value) {
|
|
2272
|
+
this.CompareRunATests = value;
|
|
2273
|
+
}
|
|
2274
|
+
/** @deprecated Use {@link CompareRunBTests}. */
|
|
2275
|
+
get compareRunBTests() {
|
|
2276
|
+
return this.CompareRunBTests;
|
|
2277
|
+
}
|
|
2278
|
+
/** @deprecated Use {@link CompareRunBTests}. */
|
|
2279
|
+
set compareRunBTests(value) {
|
|
2280
|
+
this.CompareRunBTests = value;
|
|
2281
|
+
}
|
|
2282
|
+
/** @deprecated Use {@link KeyboardShortcutsEnabled}. */
|
|
2283
|
+
get keyboardShortcutsEnabled() {
|
|
2284
|
+
return this.KeyboardShortcutsEnabled;
|
|
2285
|
+
}
|
|
2286
|
+
/** @deprecated Use {@link KeyboardShortcutsEnabled}. */
|
|
2287
|
+
set keyboardShortcutsEnabled(value) {
|
|
2288
|
+
this.KeyboardShortcutsEnabled = value;
|
|
2289
|
+
}
|
|
2290
|
+
/** @deprecated Use {@link ShowShortcuts}. */
|
|
2291
|
+
get showShortcuts() {
|
|
2292
|
+
return this.ShowShortcuts;
|
|
2293
|
+
}
|
|
2294
|
+
/** @deprecated Use {@link ShowShortcuts}. */
|
|
2295
|
+
set showShortcuts(value) {
|
|
2296
|
+
this.ShowShortcuts = value;
|
|
2297
|
+
} // Hidden by default
|
|
2082
2298
|
get metadata() { return this.ProviderToUse; }
|
|
2299
|
+
/** @deprecated Use {@link EvalPreferences}. */
|
|
2300
|
+
get evalPreferences() {
|
|
2301
|
+
return this.EvalPreferences;
|
|
2302
|
+
}
|
|
2303
|
+
/** @deprecated Use {@link EvalPreferences}. */
|
|
2304
|
+
set evalPreferences(value) {
|
|
2305
|
+
this.EvalPreferences = value;
|
|
2306
|
+
}
|
|
2307
|
+
/** @deprecated Use {@link FiltersCollapsed}. */
|
|
2308
|
+
get filtersCollapsed() {
|
|
2309
|
+
return this.FiltersCollapsed;
|
|
2310
|
+
}
|
|
2311
|
+
/** @deprecated Use {@link FiltersCollapsed}. */
|
|
2312
|
+
set filtersCollapsed(value) {
|
|
2313
|
+
this.FiltersCollapsed = value;
|
|
2314
|
+
}
|
|
2315
|
+
/** @deprecated Use {@link MatrixSortBy}. */
|
|
2316
|
+
get matrixSortBy() {
|
|
2317
|
+
return this.MatrixSortBy;
|
|
2318
|
+
}
|
|
2319
|
+
/** @deprecated Use {@link MatrixSortBy}. */
|
|
2320
|
+
set matrixSortBy(value) {
|
|
2321
|
+
this.MatrixSortBy = value;
|
|
2322
|
+
}
|
|
2323
|
+
/** @deprecated Use {@link MatrixSortAsc}. */
|
|
2324
|
+
get matrixSortAsc() {
|
|
2325
|
+
return this.MatrixSortAsc;
|
|
2326
|
+
}
|
|
2327
|
+
/** @deprecated Use {@link MatrixSortAsc}. */
|
|
2328
|
+
set matrixSortAsc(value) {
|
|
2329
|
+
this.MatrixSortAsc = value;
|
|
2330
|
+
}
|
|
2331
|
+
/** @deprecated Use {@link SelectedMatrixTestId}. */
|
|
2332
|
+
get selectedMatrixTestId() {
|
|
2333
|
+
return this.SelectedMatrixTestId;
|
|
2334
|
+
}
|
|
2335
|
+
/** @deprecated Use {@link SelectedMatrixTestId}. */
|
|
2336
|
+
set selectedMatrixTestId(value) {
|
|
2337
|
+
this.SelectedMatrixTestId = value;
|
|
2338
|
+
}
|
|
2339
|
+
/** @deprecated Use {@link MatrixTestFilter}. */
|
|
2340
|
+
get matrixTestFilter() {
|
|
2341
|
+
return this.MatrixTestFilter;
|
|
2342
|
+
}
|
|
2343
|
+
/** @deprecated Use {@link MatrixTestFilter}. */
|
|
2344
|
+
set matrixTestFilter(value) {
|
|
2345
|
+
this.MatrixTestFilter = value;
|
|
2346
|
+
}
|
|
2347
|
+
/** @deprecated Use {@link IsSaving}. */
|
|
2348
|
+
get isSaving() {
|
|
2349
|
+
return this.IsSaving;
|
|
2350
|
+
}
|
|
2351
|
+
/** @deprecated Use {@link IsSaving}. */
|
|
2352
|
+
set isSaving(value) {
|
|
2353
|
+
this.IsSaving = value;
|
|
2354
|
+
}
|
|
2355
|
+
/** @deprecated Use {@link ParentSuiteOptions}. */
|
|
2356
|
+
get parentSuiteOptions() {
|
|
2357
|
+
return this.ParentSuiteOptions;
|
|
2358
|
+
}
|
|
2359
|
+
/** @deprecated Use {@link ParentSuiteOptions}. */
|
|
2360
|
+
set parentSuiteOptions(value) {
|
|
2361
|
+
this.ParentSuiteOptions = value;
|
|
2362
|
+
}
|
|
2363
|
+
/** @deprecated Use {@link TagDraft}. */
|
|
2364
|
+
get tagDraft() {
|
|
2365
|
+
return this.TagDraft;
|
|
2366
|
+
}
|
|
2367
|
+
/** @deprecated Use {@link TagDraft}. */
|
|
2368
|
+
set tagDraft(value) {
|
|
2369
|
+
this.TagDraft = value;
|
|
2370
|
+
}
|
|
2371
|
+
/** @deprecated Use {@link StatusOptions}. */
|
|
2372
|
+
get statusOptions() {
|
|
2373
|
+
return this.StatusOptions;
|
|
2374
|
+
}
|
|
2375
|
+
/** @deprecated Use {@link ShowAddTestsDialog}. */
|
|
2376
|
+
get showAddTestsDialog() {
|
|
2377
|
+
return this.ShowAddTestsDialog;
|
|
2378
|
+
}
|
|
2379
|
+
/** @deprecated Use {@link ShowAddTestsDialog}. */
|
|
2380
|
+
set showAddTestsDialog(value) {
|
|
2381
|
+
this.ShowAddTestsDialog = value;
|
|
2382
|
+
}
|
|
2383
|
+
/** @deprecated Use {@link AvailableTests}. */
|
|
2384
|
+
get availableTests() {
|
|
2385
|
+
return this.AvailableTests;
|
|
2386
|
+
}
|
|
2387
|
+
/** @deprecated Use {@link AvailableTests}. */
|
|
2388
|
+
set availableTests(value) {
|
|
2389
|
+
this.AvailableTests = value;
|
|
2390
|
+
}
|
|
2391
|
+
/** @deprecated Use {@link LoadingAvailableTests}. */
|
|
2392
|
+
get loadingAvailableTests() {
|
|
2393
|
+
return this.LoadingAvailableTests;
|
|
2394
|
+
}
|
|
2395
|
+
/** @deprecated Use {@link LoadingAvailableTests}. */
|
|
2396
|
+
set loadingAvailableTests(value) {
|
|
2397
|
+
this.LoadingAvailableTests = value;
|
|
2398
|
+
}
|
|
2399
|
+
/** @deprecated Use {@link SelectedTestIdsToAdd}. */
|
|
2400
|
+
get selectedTestIdsToAdd() {
|
|
2401
|
+
return this.SelectedTestIdsToAdd;
|
|
2402
|
+
}
|
|
2403
|
+
/** @deprecated Use {@link SelectedTestIdsToAdd}. */
|
|
2404
|
+
set selectedTestIdsToAdd(value) {
|
|
2405
|
+
this.SelectedTestIdsToAdd = value;
|
|
2406
|
+
}
|
|
2407
|
+
/** @deprecated Use {@link AddTestsSearch}. */
|
|
2408
|
+
get addTestsSearch() {
|
|
2409
|
+
return this.AddTestsSearch;
|
|
2410
|
+
}
|
|
2411
|
+
/** @deprecated Use {@link AddTestsSearch}. */
|
|
2412
|
+
set addTestsSearch(value) {
|
|
2413
|
+
this.AddTestsSearch = value;
|
|
2414
|
+
}
|
|
2415
|
+
/** @deprecated Use {@link IsAddingTests}. */
|
|
2416
|
+
get isAddingTests() {
|
|
2417
|
+
return this.IsAddingTests;
|
|
2418
|
+
}
|
|
2419
|
+
/** @deprecated Use {@link IsAddingTests}. */
|
|
2420
|
+
set isAddingTests(value) {
|
|
2421
|
+
this.IsAddingTests = value;
|
|
2422
|
+
}
|
|
2423
|
+
/** @deprecated Use {@link ConfirmingRemoveSuiteTestId}. */
|
|
2424
|
+
get confirmingRemoveSuiteTestId() {
|
|
2425
|
+
return this.ConfirmingRemoveSuiteTestId;
|
|
2426
|
+
}
|
|
2427
|
+
/** @deprecated Use {@link ConfirmingRemoveSuiteTestId}. */
|
|
2428
|
+
set confirmingRemoveSuiteTestId(value) {
|
|
2429
|
+
this.ConfirmingRemoveSuiteTestId = value;
|
|
2430
|
+
}
|
|
2431
|
+
/** @deprecated Use {@link IsRemovingTest}. */
|
|
2432
|
+
get isRemovingTest() {
|
|
2433
|
+
return this.IsRemovingTest;
|
|
2434
|
+
}
|
|
2435
|
+
/** @deprecated Use {@link IsRemovingTest}. */
|
|
2436
|
+
set isRemovingTest(value) {
|
|
2437
|
+
this.IsRemovingTest = value;
|
|
2438
|
+
}
|
|
2439
|
+
/** @deprecated Use {@link IsReorderingTests}. */
|
|
2440
|
+
get isReorderingTests() {
|
|
2441
|
+
return this.IsReorderingTests;
|
|
2442
|
+
}
|
|
2443
|
+
/** @deprecated Use {@link IsReorderingTests}. */
|
|
2444
|
+
set isReorderingTests(value) {
|
|
2445
|
+
this.IsReorderingTests = value;
|
|
2446
|
+
}
|
|
2447
|
+
/** @deprecated Use {@link TestingDialogService}. */
|
|
2448
|
+
get testingDialogService() {
|
|
2449
|
+
return this.TestingDialogService;
|
|
2450
|
+
}
|
|
2451
|
+
/** @deprecated Use {@link TestingDialogService}. */
|
|
2452
|
+
set testingDialogService(value) {
|
|
2453
|
+
this.TestingDialogService = value;
|
|
2454
|
+
}
|
|
2083
2455
|
async ngOnInit() {
|
|
2084
2456
|
await super.ngOnInit();
|
|
2085
2457
|
this.loadShortcutsSetting();
|
|
@@ -2089,10 +2461,10 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2089
2461
|
this.evalPrefsService.preferences$
|
|
2090
2462
|
.pipe(takeUntil(this.destroy$))
|
|
2091
2463
|
.subscribe(prefs => {
|
|
2092
|
-
this.
|
|
2464
|
+
this.EvalPreferences = prefs;
|
|
2093
2465
|
this.cdr.markForCheck();
|
|
2094
2466
|
// Re-render chart when preferences change (D3 chart needs manual update)
|
|
2095
|
-
if (this.chartRendered && this.
|
|
2467
|
+
if (this.chartRendered && this.AnalyticsView === 'chart') {
|
|
2096
2468
|
this.renderChart();
|
|
2097
2469
|
}
|
|
2098
2470
|
});
|
|
@@ -2100,11 +2472,11 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2100
2472
|
this.matrixFilterSubject$
|
|
2101
2473
|
.pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$))
|
|
2102
2474
|
.subscribe(value => {
|
|
2103
|
-
this.
|
|
2475
|
+
this.MatrixTestFilter = value;
|
|
2104
2476
|
this.cdr.markForCheck();
|
|
2105
2477
|
});
|
|
2106
2478
|
// Subscribe to panel state changes so the slide panel renders in this form
|
|
2107
|
-
this.
|
|
2479
|
+
this.TestingDialogService.PanelStateChanged$
|
|
2108
2480
|
.pipe(takeUntil(this.destroy$))
|
|
2109
2481
|
.subscribe(() => {
|
|
2110
2482
|
this.cdr.detectChanges();
|
|
@@ -2119,52 +2491,52 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2119
2491
|
}
|
|
2120
2492
|
// Keyboard shortcuts
|
|
2121
2493
|
handleKeyboardShortcut(event) {
|
|
2122
|
-
if (!this.
|
|
2494
|
+
if (!this.KeyboardShortcutsEnabled)
|
|
2123
2495
|
return;
|
|
2124
2496
|
// Cmd/Ctrl + S: Save (if dirty)
|
|
2125
2497
|
if ((event.metaKey || event.ctrlKey) && event.key === 's' && !event.shiftKey) {
|
|
2126
|
-
if (this.
|
|
2498
|
+
if (this.IsDirty) {
|
|
2127
2499
|
event.preventDefault();
|
|
2128
|
-
this.
|
|
2500
|
+
this.SaveChanges();
|
|
2129
2501
|
}
|
|
2130
2502
|
return;
|
|
2131
2503
|
}
|
|
2132
2504
|
// Cmd/Ctrl + R: Refresh
|
|
2133
2505
|
if ((event.metaKey || event.ctrlKey) && event.key === 'r' && !event.shiftKey) {
|
|
2134
2506
|
event.preventDefault();
|
|
2135
|
-
this.
|
|
2507
|
+
this.Refresh();
|
|
2136
2508
|
return;
|
|
2137
2509
|
}
|
|
2138
2510
|
// Cmd/Ctrl + Enter: Run suite
|
|
2139
2511
|
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
|
2140
2512
|
event.preventDefault();
|
|
2141
|
-
this.
|
|
2513
|
+
this.RunSuite();
|
|
2142
2514
|
return;
|
|
2143
2515
|
}
|
|
2144
2516
|
// Number keys for tabs (1-5) — skip when typing into form inputs
|
|
2145
2517
|
if (!event.metaKey && !event.ctrlKey && !event.altKey && !this.isTextInputFocused()) {
|
|
2146
2518
|
switch (event.key) {
|
|
2147
2519
|
case '1':
|
|
2148
|
-
this.
|
|
2520
|
+
this.ChangeTab('overview');
|
|
2149
2521
|
break;
|
|
2150
2522
|
case '2':
|
|
2151
|
-
this.
|
|
2523
|
+
this.ChangeTab('tests');
|
|
2152
2524
|
break;
|
|
2153
2525
|
case '3':
|
|
2154
|
-
this.
|
|
2526
|
+
this.ChangeTab('runs');
|
|
2155
2527
|
break;
|
|
2156
2528
|
case '4':
|
|
2157
|
-
this.
|
|
2529
|
+
this.ChangeTab('analytics');
|
|
2158
2530
|
break;
|
|
2159
2531
|
case '5':
|
|
2160
|
-
this.
|
|
2532
|
+
this.ChangeTab('compare');
|
|
2161
2533
|
break;
|
|
2162
2534
|
}
|
|
2163
2535
|
}
|
|
2164
2536
|
}
|
|
2165
2537
|
// Warn before tab close / hard navigation when there are unsaved changes
|
|
2166
2538
|
handleBeforeUnload(event) {
|
|
2167
|
-
if (this.
|
|
2539
|
+
if (this.IsDirty) {
|
|
2168
2540
|
event.preventDefault();
|
|
2169
2541
|
event.returnValue = '';
|
|
2170
2542
|
}
|
|
@@ -2176,20 +2548,24 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2176
2548
|
const tag = el.tagName;
|
|
2177
2549
|
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable;
|
|
2178
2550
|
}
|
|
2179
|
-
|
|
2180
|
-
this.
|
|
2181
|
-
if (tab === 'tests' && !this.
|
|
2551
|
+
ChangeTab(tab) {
|
|
2552
|
+
this.ActiveTab = tab;
|
|
2553
|
+
if (tab === 'tests' && !this.TestsLoaded)
|
|
2182
2554
|
this.loadTests();
|
|
2183
|
-
if (tab === 'runs' && !this.
|
|
2555
|
+
if (tab === 'runs' && !this.RunsLoaded)
|
|
2184
2556
|
this.loadRuns();
|
|
2185
|
-
if (tab === 'analytics' && !this.
|
|
2557
|
+
if (tab === 'analytics' && !this.AnalyticsLoaded)
|
|
2186
2558
|
this.loadAnalytics();
|
|
2187
2559
|
this.cdr.markForCheck();
|
|
2188
2560
|
}
|
|
2561
|
+
/** @deprecated Use {@link ChangeTab}. */
|
|
2562
|
+
changeTab(tab) {
|
|
2563
|
+
return this.ChangeTab(tab);
|
|
2564
|
+
}
|
|
2189
2565
|
async loadTests() {
|
|
2190
|
-
if (this.
|
|
2566
|
+
if (this.TestsLoaded)
|
|
2191
2567
|
return;
|
|
2192
|
-
this.
|
|
2568
|
+
this.LoadingTests = true;
|
|
2193
2569
|
this.cdr.markForCheck();
|
|
2194
2570
|
try {
|
|
2195
2571
|
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
@@ -2200,22 +2576,22 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2200
2576
|
ResultType: 'entity_object'
|
|
2201
2577
|
});
|
|
2202
2578
|
if (result.Success)
|
|
2203
|
-
this.
|
|
2204
|
-
this.
|
|
2579
|
+
this.SuiteTests = result.Results || [];
|
|
2580
|
+
this.TestsLoaded = true;
|
|
2205
2581
|
}
|
|
2206
2582
|
catch (error) {
|
|
2207
2583
|
console.error('Error loading tests:', error);
|
|
2208
2584
|
SharedService.Instance.CreateSimpleNotification('Failed to load tests', 'error', 3000);
|
|
2209
2585
|
}
|
|
2210
2586
|
finally {
|
|
2211
|
-
this.
|
|
2587
|
+
this.LoadingTests = false;
|
|
2212
2588
|
this.cdr.markForCheck();
|
|
2213
2589
|
}
|
|
2214
2590
|
}
|
|
2215
2591
|
async loadRuns() {
|
|
2216
|
-
if (this.
|
|
2592
|
+
if (this.RunsLoaded)
|
|
2217
2593
|
return;
|
|
2218
|
-
this.
|
|
2594
|
+
this.LoadingRuns = true;
|
|
2219
2595
|
this.cdr.markForCheck();
|
|
2220
2596
|
try {
|
|
2221
2597
|
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
@@ -2227,19 +2603,19 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2227
2603
|
ResultType: 'entity_object'
|
|
2228
2604
|
});
|
|
2229
2605
|
if (result.Success)
|
|
2230
|
-
this.
|
|
2231
|
-
this.
|
|
2606
|
+
this.SuiteRuns = result.Results || [];
|
|
2607
|
+
this.RunsLoaded = true;
|
|
2232
2608
|
}
|
|
2233
2609
|
catch (error) {
|
|
2234
2610
|
console.error('Error loading runs:', error);
|
|
2235
2611
|
SharedService.Instance.CreateSimpleNotification('Failed to load runs', 'error', 3000);
|
|
2236
2612
|
}
|
|
2237
2613
|
finally {
|
|
2238
|
-
this.
|
|
2614
|
+
this.LoadingRuns = false;
|
|
2239
2615
|
this.cdr.markForCheck();
|
|
2240
2616
|
}
|
|
2241
2617
|
}
|
|
2242
|
-
|
|
2618
|
+
GetStatusColor() {
|
|
2243
2619
|
switch (this.record.Status) {
|
|
2244
2620
|
case 'Active': return '#10b981';
|
|
2245
2621
|
case 'Disabled': return '#6b7280';
|
|
@@ -2247,10 +2623,18 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2247
2623
|
default: return '#9ca3af';
|
|
2248
2624
|
}
|
|
2249
2625
|
}
|
|
2250
|
-
|
|
2626
|
+
/** @deprecated Use {@link GetStatusColor}. */
|
|
2627
|
+
getStatusColor() {
|
|
2628
|
+
return this.GetStatusColor();
|
|
2629
|
+
}
|
|
2630
|
+
GetStatusClass() {
|
|
2251
2631
|
return `status-${this.record.Status?.toLowerCase() || 'unknown'}`;
|
|
2252
2632
|
}
|
|
2253
|
-
|
|
2633
|
+
/** @deprecated Use {@link GetStatusClass}. */
|
|
2634
|
+
getStatusClass() {
|
|
2635
|
+
return this.GetStatusClass();
|
|
2636
|
+
}
|
|
2637
|
+
GetRunStatusColor(status) {
|
|
2254
2638
|
switch (status) {
|
|
2255
2639
|
case 'Completed': return '#10b981';
|
|
2256
2640
|
case 'Failed': return '#ef4444';
|
|
@@ -2260,7 +2644,11 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2260
2644
|
default: return '#9ca3af';
|
|
2261
2645
|
}
|
|
2262
2646
|
}
|
|
2263
|
-
|
|
2647
|
+
/** @deprecated Use {@link GetRunStatusColor}. */
|
|
2648
|
+
getRunStatusColor(status) {
|
|
2649
|
+
return this.GetRunStatusColor(status);
|
|
2650
|
+
}
|
|
2651
|
+
FormatTimeout(ms) {
|
|
2264
2652
|
if (ms === null || ms === undefined)
|
|
2265
2653
|
return 'Default (5 min)';
|
|
2266
2654
|
if (ms < 1000)
|
|
@@ -2276,7 +2664,11 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2276
2664
|
const mins = Math.floor((ms % 3600000) / 60000);
|
|
2277
2665
|
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
2278
2666
|
}
|
|
2279
|
-
|
|
2667
|
+
/** @deprecated Use {@link FormatTimeout}. */
|
|
2668
|
+
formatTimeout(ms) {
|
|
2669
|
+
return this.FormatTimeout(ms);
|
|
2670
|
+
}
|
|
2671
|
+
GetRelativeTime(date) {
|
|
2280
2672
|
if (!date)
|
|
2281
2673
|
return 'N/A';
|
|
2282
2674
|
const d = new Date(date);
|
|
@@ -2295,59 +2687,83 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2295
2687
|
return `${diffDays}d ago`;
|
|
2296
2688
|
return d.toLocaleDateString();
|
|
2297
2689
|
}
|
|
2298
|
-
|
|
2690
|
+
/** @deprecated Use {@link GetRelativeTime}. */
|
|
2691
|
+
getRelativeTime(date) {
|
|
2692
|
+
return this.GetRelativeTime(date);
|
|
2693
|
+
}
|
|
2694
|
+
GetPassRate(run) {
|
|
2299
2695
|
const total = run.TotalTests || 0;
|
|
2300
2696
|
const passed = run.PassedTests || 0;
|
|
2301
2697
|
if (total === 0)
|
|
2302
2698
|
return 0;
|
|
2303
2699
|
return (passed / total) * 100;
|
|
2304
2700
|
}
|
|
2305
|
-
|
|
2701
|
+
/** @deprecated Use {@link GetPassRate}. */
|
|
2702
|
+
getPassRate(run) {
|
|
2703
|
+
return this.GetPassRate(run);
|
|
2704
|
+
}
|
|
2705
|
+
OpenTest(testId) {
|
|
2306
2706
|
SharedService.Instance.OpenEntityRecord('MJ: Tests', CompositeKey.FromID(testId));
|
|
2307
2707
|
}
|
|
2308
|
-
|
|
2708
|
+
/** @deprecated Use {@link OpenTest}. */
|
|
2709
|
+
openTest(testId) {
|
|
2710
|
+
return this.OpenTest(testId);
|
|
2711
|
+
}
|
|
2712
|
+
OpenSuiteRun(runId) {
|
|
2309
2713
|
SharedService.Instance.OpenEntityRecord('MJ: Test Suite Runs', CompositeKey.FromID(runId));
|
|
2310
2714
|
}
|
|
2311
|
-
|
|
2715
|
+
/** @deprecated Use {@link OpenSuiteRun}. */
|
|
2716
|
+
openSuiteRun(runId) {
|
|
2717
|
+
return this.OpenSuiteRun(runId);
|
|
2718
|
+
}
|
|
2719
|
+
NavigateToTestingDashboard() {
|
|
2312
2720
|
const testingApp = this.appManager.GetAppByName('Testing');
|
|
2313
2721
|
if (testingApp) {
|
|
2314
2722
|
this.navigationService.SwitchToApp(testingApp.ID);
|
|
2315
2723
|
}
|
|
2316
2724
|
}
|
|
2317
|
-
|
|
2725
|
+
/** @deprecated Use {@link NavigateToTestingDashboard}. */
|
|
2726
|
+
navigateToTestingDashboard() {
|
|
2727
|
+
return this.NavigateToTestingDashboard();
|
|
2728
|
+
}
|
|
2729
|
+
async RunSuite() {
|
|
2318
2730
|
if (this.record?.ID) {
|
|
2319
|
-
this.
|
|
2731
|
+
this.TestingDialogService.OpenSuitePanel(this.record.ID);
|
|
2320
2732
|
}
|
|
2321
2733
|
}
|
|
2734
|
+
/** @deprecated Use {@link RunSuite}. */
|
|
2735
|
+
async runSuite() {
|
|
2736
|
+
return this.RunSuite();
|
|
2737
|
+
}
|
|
2322
2738
|
OnPanelClosed() {
|
|
2323
|
-
this.
|
|
2739
|
+
this.TestingDialogService.ClosePanel();
|
|
2324
2740
|
this.cdr.markForCheck();
|
|
2325
2741
|
}
|
|
2326
|
-
async
|
|
2742
|
+
async Refresh() {
|
|
2327
2743
|
this.isRefreshing = true;
|
|
2328
2744
|
this.cdr.markForCheck();
|
|
2329
2745
|
try {
|
|
2330
2746
|
await this.record.Load(this.record.ID);
|
|
2331
2747
|
// Reset lazy-loaded data
|
|
2332
|
-
if (this.
|
|
2333
|
-
this.
|
|
2334
|
-
this.
|
|
2748
|
+
if (this.TestsLoaded) {
|
|
2749
|
+
this.TestsLoaded = false;
|
|
2750
|
+
this.SuiteTests = [];
|
|
2335
2751
|
await this.loadTests();
|
|
2336
2752
|
}
|
|
2337
|
-
if (this.
|
|
2338
|
-
this.
|
|
2339
|
-
this.
|
|
2753
|
+
if (this.RunsLoaded) {
|
|
2754
|
+
this.RunsLoaded = false;
|
|
2755
|
+
this.SuiteRuns = [];
|
|
2340
2756
|
await this.loadRuns();
|
|
2341
2757
|
}
|
|
2342
|
-
if (this.
|
|
2343
|
-
this.
|
|
2344
|
-
this.
|
|
2758
|
+
if (this.AnalyticsLoaded) {
|
|
2759
|
+
this.AnalyticsLoaded = false;
|
|
2760
|
+
this.AnalyticsData = [];
|
|
2345
2761
|
// Also reset matrix data so it reloads with fresh data
|
|
2346
|
-
this.
|
|
2347
|
-
this.
|
|
2762
|
+
this.MatrixLoaded = false;
|
|
2763
|
+
this.MatrixData = [];
|
|
2348
2764
|
await this.loadAnalytics();
|
|
2349
2765
|
// Reload matrix if currently viewing matrix or chart view
|
|
2350
|
-
if (this.
|
|
2766
|
+
if (this.AnalyticsView === 'matrix' || this.AnalyticsView === 'chart') {
|
|
2351
2767
|
await this.loadMatrixData();
|
|
2352
2768
|
}
|
|
2353
2769
|
}
|
|
@@ -2361,13 +2777,17 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2361
2777
|
this.cdr.markForCheck();
|
|
2362
2778
|
}
|
|
2363
2779
|
}
|
|
2780
|
+
/** @deprecated Use {@link Refresh}. */
|
|
2781
|
+
async refresh() {
|
|
2782
|
+
return this.Refresh();
|
|
2783
|
+
}
|
|
2364
2784
|
// ==========================================
|
|
2365
2785
|
// Analytics Tab Methods
|
|
2366
2786
|
// ==========================================
|
|
2367
2787
|
async loadAnalytics() {
|
|
2368
|
-
if (this.
|
|
2788
|
+
if (this.AnalyticsLoaded)
|
|
2369
2789
|
return;
|
|
2370
|
-
this.
|
|
2790
|
+
this.LoadingAnalytics = true;
|
|
2371
2791
|
this.cdr.markForCheck();
|
|
2372
2792
|
try {
|
|
2373
2793
|
// Load all runs for analytics (not just recent 50)
|
|
@@ -2381,23 +2801,23 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2381
2801
|
});
|
|
2382
2802
|
if (result.Success && result.Results) {
|
|
2383
2803
|
// Process runs into analytics data points
|
|
2384
|
-
this.
|
|
2804
|
+
this.AnalyticsData = result.Results.map(run => this.runToDataPoint(run));
|
|
2385
2805
|
// Extract unique tags
|
|
2386
|
-
this.
|
|
2806
|
+
this.UniqueTags = TagsHelper.getUniqueTags(result.Results.map(r => r.Tags));
|
|
2387
2807
|
// Also populate suiteRuns if not already loaded
|
|
2388
|
-
if (!this.
|
|
2389
|
-
this.
|
|
2390
|
-
this.
|
|
2808
|
+
if (!this.RunsLoaded) {
|
|
2809
|
+
this.SuiteRuns = result.Results.slice(0, 50);
|
|
2810
|
+
this.RunsLoaded = true;
|
|
2391
2811
|
}
|
|
2392
2812
|
}
|
|
2393
|
-
this.
|
|
2813
|
+
this.AnalyticsLoaded = true;
|
|
2394
2814
|
}
|
|
2395
2815
|
catch (error) {
|
|
2396
2816
|
console.error('Error loading analytics:', error);
|
|
2397
2817
|
SharedService.Instance.CreateSimpleNotification('Failed to load analytics data', 'error', 3000);
|
|
2398
2818
|
}
|
|
2399
2819
|
finally {
|
|
2400
|
-
this.
|
|
2820
|
+
this.LoadingAnalytics = false;
|
|
2401
2821
|
this.cdr.markForCheck();
|
|
2402
2822
|
}
|
|
2403
2823
|
}
|
|
@@ -2419,12 +2839,12 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2419
2839
|
status: run.Status || 'Unknown'
|
|
2420
2840
|
};
|
|
2421
2841
|
}
|
|
2422
|
-
|
|
2423
|
-
let data = this.
|
|
2842
|
+
GetFilteredAnalyticsData() {
|
|
2843
|
+
let data = this.AnalyticsData;
|
|
2424
2844
|
// Apply time range filter
|
|
2425
2845
|
const now = new Date();
|
|
2426
2846
|
let cutoffDate = null;
|
|
2427
|
-
switch (this.
|
|
2847
|
+
switch (this.AnalyticsTimeRange) {
|
|
2428
2848
|
case '7d':
|
|
2429
2849
|
cutoffDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
2430
2850
|
break;
|
|
@@ -2439,76 +2859,96 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2439
2859
|
data = data.filter(d => d.date >= cutoffDate);
|
|
2440
2860
|
}
|
|
2441
2861
|
// Apply tag filter (multi-select: empty array means all tags)
|
|
2442
|
-
if (this.
|
|
2443
|
-
data = data.filter(d => this.
|
|
2862
|
+
if (this.SelectedTags.length > 0) {
|
|
2863
|
+
data = data.filter(d => this.SelectedTags.some(tag => d.tags.includes(tag)));
|
|
2444
2864
|
}
|
|
2445
2865
|
return data;
|
|
2446
2866
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2867
|
+
/** @deprecated Use {@link GetFilteredAnalyticsData}. */
|
|
2868
|
+
getFilteredAnalyticsData() {
|
|
2869
|
+
return this.GetFilteredAnalyticsData();
|
|
2870
|
+
}
|
|
2871
|
+
SetTimeRange(range) {
|
|
2872
|
+
this.AnalyticsTimeRange = range;
|
|
2449
2873
|
// Reload matrix data when time range changes (if currently viewing matrix or chart)
|
|
2450
|
-
if (this.
|
|
2874
|
+
if (this.AnalyticsView === 'matrix' || this.AnalyticsView === 'chart') {
|
|
2451
2875
|
this.reloadMatrixData();
|
|
2452
2876
|
}
|
|
2453
2877
|
this.cdr.markForCheck();
|
|
2454
2878
|
}
|
|
2879
|
+
/** @deprecated Use {@link SetTimeRange}. */
|
|
2880
|
+
setTimeRange(range) {
|
|
2881
|
+
return this.SetTimeRange(range);
|
|
2882
|
+
}
|
|
2455
2883
|
/**
|
|
2456
2884
|
* Toggle a tag in the multi-select filter.
|
|
2457
2885
|
* If tag is null, clear all selections (show all tags).
|
|
2458
2886
|
*/
|
|
2459
|
-
|
|
2887
|
+
ToggleTagFilter(tag) {
|
|
2460
2888
|
if (tag === null) {
|
|
2461
2889
|
// Clear all - show all tags
|
|
2462
|
-
this.
|
|
2890
|
+
this.SelectedTags = [];
|
|
2463
2891
|
}
|
|
2464
2892
|
else {
|
|
2465
2893
|
// Toggle the tag
|
|
2466
|
-
const index = this.
|
|
2894
|
+
const index = this.SelectedTags.indexOf(tag);
|
|
2467
2895
|
if (index >= 0) {
|
|
2468
|
-
this.
|
|
2896
|
+
this.SelectedTags = this.SelectedTags.filter(t => t !== tag);
|
|
2469
2897
|
}
|
|
2470
2898
|
else {
|
|
2471
|
-
this.
|
|
2899
|
+
this.SelectedTags = [...this.SelectedTags, tag];
|
|
2472
2900
|
}
|
|
2473
2901
|
}
|
|
2474
2902
|
// Reload matrix data when tag filter changes (if currently viewing matrix or chart)
|
|
2475
|
-
if (this.
|
|
2903
|
+
if (this.AnalyticsView === 'matrix' || this.AnalyticsView === 'chart') {
|
|
2476
2904
|
this.reloadMatrixData();
|
|
2477
2905
|
}
|
|
2478
2906
|
this.cdr.markForCheck();
|
|
2479
2907
|
}
|
|
2908
|
+
/** @deprecated Use {@link ToggleTagFilter}. */
|
|
2909
|
+
toggleTagFilter(tag) {
|
|
2910
|
+
return this.ToggleTagFilter(tag);
|
|
2911
|
+
}
|
|
2480
2912
|
/**
|
|
2481
2913
|
* Check if a tag is currently selected in the filter
|
|
2482
2914
|
*/
|
|
2915
|
+
IsTagSelected(tag) {
|
|
2916
|
+
return this.SelectedTags.includes(tag);
|
|
2917
|
+
}
|
|
2918
|
+
/** @deprecated Use {@link IsTagSelected}. */
|
|
2483
2919
|
isTagSelected(tag) {
|
|
2484
|
-
return this.
|
|
2920
|
+
return this.IsTagSelected(tag);
|
|
2485
2921
|
}
|
|
2486
|
-
|
|
2487
|
-
this.
|
|
2488
|
-
if ((view === 'matrix' || view === 'chart') && !this.
|
|
2922
|
+
SetAnalyticsView(view) {
|
|
2923
|
+
this.AnalyticsView = view;
|
|
2924
|
+
if ((view === 'matrix' || view === 'chart') && !this.MatrixLoaded) {
|
|
2489
2925
|
this.loadMatrixData();
|
|
2490
2926
|
}
|
|
2491
2927
|
// Render chart when switching to chart view
|
|
2492
|
-
if (view === 'chart' && this.
|
|
2928
|
+
if (view === 'chart' && this.MatrixLoaded) {
|
|
2493
2929
|
setTimeout(() => this.renderChart(), 100);
|
|
2494
2930
|
}
|
|
2495
2931
|
this.cdr.markForCheck();
|
|
2496
2932
|
}
|
|
2933
|
+
/** @deprecated Use {@link SetAnalyticsView}. */
|
|
2934
|
+
setAnalyticsView(view) {
|
|
2935
|
+
return this.SetAnalyticsView(view);
|
|
2936
|
+
}
|
|
2497
2937
|
/**
|
|
2498
2938
|
* Force reload of matrix data (used when filters change)
|
|
2499
2939
|
*/
|
|
2500
2940
|
reloadMatrixData() {
|
|
2501
|
-
this.
|
|
2941
|
+
this.MatrixLoaded = false;
|
|
2502
2942
|
this.loadMatrixData();
|
|
2503
2943
|
}
|
|
2504
2944
|
async loadMatrixData() {
|
|
2505
|
-
if (this.
|
|
2945
|
+
if (this.LoadingMatrix)
|
|
2506
2946
|
return;
|
|
2507
|
-
this.
|
|
2947
|
+
this.LoadingMatrix = true;
|
|
2508
2948
|
this.cdr.markForCheck();
|
|
2509
2949
|
try {
|
|
2510
2950
|
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
2511
|
-
const filteredRuns = this.
|
|
2951
|
+
const filteredRuns = this.GetFilteredAnalyticsData();
|
|
2512
2952
|
// Load test runs for each suite run (limit to most recent 10 for performance)
|
|
2513
2953
|
const runsToLoad = filteredRuns.slice(0, 10);
|
|
2514
2954
|
const matrixData = [];
|
|
@@ -2567,10 +3007,10 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2567
3007
|
});
|
|
2568
3008
|
}
|
|
2569
3009
|
}
|
|
2570
|
-
this.
|
|
2571
|
-
this.
|
|
3010
|
+
this.MatrixData = matrixData;
|
|
3011
|
+
this.MatrixLoaded = true;
|
|
2572
3012
|
// Render chart if currently on chart view
|
|
2573
|
-
if (this.
|
|
3013
|
+
if (this.AnalyticsView === 'chart') {
|
|
2574
3014
|
setTimeout(() => this.renderChart(), 100);
|
|
2575
3015
|
}
|
|
2576
3016
|
}
|
|
@@ -2579,7 +3019,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2579
3019
|
SharedService.Instance.CreateSimpleNotification('Failed to load matrix data', 'error', 3000);
|
|
2580
3020
|
}
|
|
2581
3021
|
finally {
|
|
2582
|
-
this.
|
|
3022
|
+
this.LoadingMatrix = false;
|
|
2583
3023
|
this.cdr.markForCheck();
|
|
2584
3024
|
}
|
|
2585
3025
|
}
|
|
@@ -2616,9 +3056,9 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2616
3056
|
}
|
|
2617
3057
|
return feedbackMap;
|
|
2618
3058
|
}
|
|
2619
|
-
|
|
3059
|
+
GetUniqueTestsFromMatrix() {
|
|
2620
3060
|
const testsMap = new Map();
|
|
2621
|
-
for (const runData of this.
|
|
3061
|
+
for (const runData of this.MatrixData) {
|
|
2622
3062
|
for (const [testId, testResult] of runData.testResults) {
|
|
2623
3063
|
if (!testsMap.has(testId)) {
|
|
2624
3064
|
testsMap.set(testId, { testName: testResult.testName, sequence: testResult.sequence });
|
|
@@ -2631,64 +3071,88 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2631
3071
|
sequence: data.sequence
|
|
2632
3072
|
}));
|
|
2633
3073
|
// Apply test name filter if set
|
|
2634
|
-
if (this.
|
|
2635
|
-
const filterLower = this.
|
|
3074
|
+
if (this.MatrixTestFilter.trim()) {
|
|
3075
|
+
const filterLower = this.MatrixTestFilter.toLowerCase().trim();
|
|
2636
3076
|
tests = tests.filter(t => t.testName.toLowerCase().includes(filterLower));
|
|
2637
3077
|
}
|
|
2638
3078
|
// Apply sorting
|
|
2639
|
-
if (this.
|
|
2640
|
-
tests = tests.sort((a, b) => this.
|
|
3079
|
+
if (this.MatrixSortBy === 'sequence') {
|
|
3080
|
+
tests = tests.sort((a, b) => this.MatrixSortAsc ? a.sequence - b.sequence : b.sequence - a.sequence);
|
|
2641
3081
|
}
|
|
2642
3082
|
else {
|
|
2643
3083
|
tests = tests.sort((a, b) => {
|
|
2644
3084
|
const cmp = a.testName.localeCompare(b.testName);
|
|
2645
|
-
return this.
|
|
3085
|
+
return this.MatrixSortAsc ? cmp : -cmp;
|
|
2646
3086
|
});
|
|
2647
3087
|
}
|
|
2648
3088
|
return tests;
|
|
2649
3089
|
}
|
|
3090
|
+
/** @deprecated Use {@link GetUniqueTestsFromMatrix}. */
|
|
3091
|
+
getUniqueTestsFromMatrix() {
|
|
3092
|
+
return this.GetUniqueTestsFromMatrix();
|
|
3093
|
+
}
|
|
2650
3094
|
/**
|
|
2651
3095
|
* Toggle matrix sort column
|
|
2652
3096
|
*/
|
|
2653
|
-
|
|
2654
|
-
if (this.
|
|
2655
|
-
this.
|
|
3097
|
+
ToggleMatrixSort(column) {
|
|
3098
|
+
if (this.MatrixSortBy === column) {
|
|
3099
|
+
this.MatrixSortAsc = !this.MatrixSortAsc;
|
|
2656
3100
|
}
|
|
2657
3101
|
else {
|
|
2658
|
-
this.
|
|
2659
|
-
this.
|
|
3102
|
+
this.MatrixSortBy = column;
|
|
3103
|
+
this.MatrixSortAsc = true;
|
|
2660
3104
|
}
|
|
2661
3105
|
this.cdr.markForCheck();
|
|
2662
3106
|
}
|
|
3107
|
+
/** @deprecated Use {@link ToggleMatrixSort}. */
|
|
3108
|
+
toggleMatrixSort(column) {
|
|
3109
|
+
return this.ToggleMatrixSort(column);
|
|
3110
|
+
}
|
|
2663
3111
|
/**
|
|
2664
3112
|
* Select/deselect a matrix row for highlighting
|
|
2665
3113
|
*/
|
|
2666
|
-
|
|
2667
|
-
this.
|
|
3114
|
+
SelectMatrixRow(testId) {
|
|
3115
|
+
this.SelectedMatrixTestId = this.SelectedMatrixTestId === testId ? null : testId;
|
|
2668
3116
|
this.cdr.markForCheck();
|
|
2669
3117
|
}
|
|
3118
|
+
/** @deprecated Use {@link SelectMatrixRow}. */
|
|
3119
|
+
selectMatrixRow(testId) {
|
|
3120
|
+
return this.SelectMatrixRow(testId);
|
|
3121
|
+
}
|
|
2670
3122
|
/**
|
|
2671
3123
|
* Handle test name filter input - uses Subject for debounce
|
|
2672
3124
|
*/
|
|
2673
|
-
|
|
3125
|
+
OnMatrixFilterInput(event) {
|
|
2674
3126
|
const value = event.target.value;
|
|
2675
3127
|
this.matrixFilterSubject$.next(value);
|
|
2676
3128
|
}
|
|
3129
|
+
/** @deprecated Use {@link OnMatrixFilterInput}. */
|
|
3130
|
+
onMatrixFilterInput(event) {
|
|
3131
|
+
return this.OnMatrixFilterInput(event);
|
|
3132
|
+
}
|
|
2677
3133
|
/**
|
|
2678
3134
|
* Clear the matrix test name filter
|
|
2679
3135
|
*/
|
|
2680
|
-
|
|
2681
|
-
this.
|
|
3136
|
+
ClearMatrixFilter() {
|
|
3137
|
+
this.MatrixTestFilter = '';
|
|
2682
3138
|
this.matrixFilterSubject$.next('');
|
|
2683
3139
|
this.cdr.markForCheck();
|
|
2684
3140
|
}
|
|
2685
|
-
|
|
2686
|
-
|
|
3141
|
+
/** @deprecated Use {@link ClearMatrixFilter}. */
|
|
3142
|
+
clearMatrixFilter() {
|
|
3143
|
+
return this.ClearMatrixFilter();
|
|
3144
|
+
}
|
|
3145
|
+
GetTestResultForRun(runId, testId) {
|
|
3146
|
+
const runData = this.MatrixData.find(r => r.runId === runId);
|
|
2687
3147
|
if (!runData)
|
|
2688
3148
|
return null;
|
|
2689
3149
|
return runData.testResults.get(testId) || null;
|
|
2690
3150
|
}
|
|
2691
|
-
|
|
3151
|
+
/** @deprecated Use {@link GetTestResultForRun}. */
|
|
3152
|
+
getTestResultForRun(runId, testId) {
|
|
3153
|
+
return this.GetTestResultForRun(runId, testId);
|
|
3154
|
+
}
|
|
3155
|
+
GetMatrixCellClass(result) {
|
|
2692
3156
|
if (!result)
|
|
2693
3157
|
return 'cell-none cell-not-run';
|
|
2694
3158
|
switch (result.status) {
|
|
@@ -2701,10 +3165,14 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2701
3165
|
default: return 'cell-pending';
|
|
2702
3166
|
}
|
|
2703
3167
|
}
|
|
3168
|
+
/** @deprecated Use {@link GetMatrixCellClass}. */
|
|
3169
|
+
getMatrixCellClass(result) {
|
|
3170
|
+
return this.GetMatrixCellClass(result);
|
|
3171
|
+
}
|
|
2704
3172
|
/**
|
|
2705
3173
|
* Get descriptive tooltip for execution status
|
|
2706
3174
|
*/
|
|
2707
|
-
|
|
3175
|
+
GetStatusTooltip(status) {
|
|
2708
3176
|
switch (status) {
|
|
2709
3177
|
case 'Passed': return 'Status: Passed - Test completed without error';
|
|
2710
3178
|
case 'Failed': return 'Status: Failed - Test assertions did not pass';
|
|
@@ -2716,10 +3184,14 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2716
3184
|
default: return `Status: ${status}`;
|
|
2717
3185
|
}
|
|
2718
3186
|
}
|
|
3187
|
+
/** @deprecated Use {@link GetStatusTooltip}. */
|
|
3188
|
+
getStatusTooltip(status) {
|
|
3189
|
+
return this.GetStatusTooltip(status);
|
|
3190
|
+
}
|
|
2719
3191
|
/**
|
|
2720
3192
|
* Get tooltip for human review with rating and optional comments
|
|
2721
3193
|
*/
|
|
2722
|
-
|
|
3194
|
+
GetHumanTooltip(rating, comments) {
|
|
2723
3195
|
let tooltip = `Human Review: ${rating}/10 rating`;
|
|
2724
3196
|
if (comments) {
|
|
2725
3197
|
const truncated = comments.length > 200 ? comments.substring(0, 200) + '...' : comments;
|
|
@@ -2727,26 +3199,34 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2727
3199
|
}
|
|
2728
3200
|
return tooltip;
|
|
2729
3201
|
}
|
|
3202
|
+
/** @deprecated Use {@link GetHumanTooltip}. */
|
|
3203
|
+
getHumanTooltip(rating, comments) {
|
|
3204
|
+
return this.GetHumanTooltip(rating, comments);
|
|
3205
|
+
}
|
|
2730
3206
|
/**
|
|
2731
3207
|
* Get the count of enabled evaluation types for matrix cell layout
|
|
2732
3208
|
*/
|
|
2733
|
-
|
|
3209
|
+
GetEvalCount() {
|
|
2734
3210
|
let count = 0;
|
|
2735
|
-
if (this.
|
|
3211
|
+
if (this.EvalPreferences.showExecution)
|
|
2736
3212
|
count++;
|
|
2737
|
-
if (this.
|
|
3213
|
+
if (this.EvalPreferences.showHuman)
|
|
2738
3214
|
count++;
|
|
2739
|
-
if (this.
|
|
3215
|
+
if (this.EvalPreferences.showAuto)
|
|
2740
3216
|
count++;
|
|
2741
3217
|
return count;
|
|
2742
3218
|
}
|
|
3219
|
+
/** @deprecated Use {@link GetEvalCount}. */
|
|
3220
|
+
getEvalCount() {
|
|
3221
|
+
return this.GetEvalCount();
|
|
3222
|
+
}
|
|
2743
3223
|
// ===========================
|
|
2744
3224
|
// Matrix Totals Row Methods
|
|
2745
3225
|
// ===========================
|
|
2746
3226
|
/**
|
|
2747
3227
|
* Get count of passed tests for a run
|
|
2748
3228
|
*/
|
|
2749
|
-
|
|
3229
|
+
GetRunPassedCount(run) {
|
|
2750
3230
|
let count = 0;
|
|
2751
3231
|
run.testResults.forEach(result => {
|
|
2752
3232
|
if (result.status === 'Passed')
|
|
@@ -2754,16 +3234,24 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2754
3234
|
});
|
|
2755
3235
|
return count;
|
|
2756
3236
|
}
|
|
3237
|
+
/** @deprecated Use {@link GetRunPassedCount}. */
|
|
3238
|
+
getRunPassedCount(run) {
|
|
3239
|
+
return this.GetRunPassedCount(run);
|
|
3240
|
+
}
|
|
2757
3241
|
/**
|
|
2758
3242
|
* Get total count of tests for a run
|
|
2759
3243
|
*/
|
|
2760
|
-
|
|
3244
|
+
GetRunTotalCount(run) {
|
|
2761
3245
|
return run.testResults.size;
|
|
2762
3246
|
}
|
|
3247
|
+
/** @deprecated Use {@link GetRunTotalCount}. */
|
|
3248
|
+
getRunTotalCount(run) {
|
|
3249
|
+
return this.GetRunTotalCount(run);
|
|
3250
|
+
}
|
|
2763
3251
|
/**
|
|
2764
3252
|
* Get average human rating for a run (only from tests that have ratings)
|
|
2765
3253
|
*/
|
|
2766
|
-
|
|
3254
|
+
GetRunHumanAvg(run) {
|
|
2767
3255
|
let sum = 0;
|
|
2768
3256
|
let count = 0;
|
|
2769
3257
|
run.testResults.forEach(result => {
|
|
@@ -2774,10 +3262,14 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2774
3262
|
});
|
|
2775
3263
|
return count > 0 ? sum / count : null;
|
|
2776
3264
|
}
|
|
3265
|
+
/** @deprecated Use {@link GetRunHumanAvg}. */
|
|
3266
|
+
getRunHumanAvg(run) {
|
|
3267
|
+
return this.GetRunHumanAvg(run);
|
|
3268
|
+
}
|
|
2777
3269
|
/**
|
|
2778
3270
|
* Get count of tests with human ratings for a run
|
|
2779
3271
|
*/
|
|
2780
|
-
|
|
3272
|
+
GetRunHumanCount(run) {
|
|
2781
3273
|
let count = 0;
|
|
2782
3274
|
run.testResults.forEach(result => {
|
|
2783
3275
|
if (result.humanRating != null)
|
|
@@ -2785,10 +3277,14 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2785
3277
|
});
|
|
2786
3278
|
return count;
|
|
2787
3279
|
}
|
|
3280
|
+
/** @deprecated Use {@link GetRunHumanCount}. */
|
|
3281
|
+
getRunHumanCount(run) {
|
|
3282
|
+
return this.GetRunHumanCount(run);
|
|
3283
|
+
}
|
|
2788
3284
|
/**
|
|
2789
3285
|
* Get average auto score for a run (only from tests that have scores)
|
|
2790
3286
|
*/
|
|
2791
|
-
|
|
3287
|
+
GetRunAutoAvg(run) {
|
|
2792
3288
|
let sum = 0;
|
|
2793
3289
|
let count = 0;
|
|
2794
3290
|
run.testResults.forEach(result => {
|
|
@@ -2799,10 +3295,14 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2799
3295
|
});
|
|
2800
3296
|
return count > 0 ? sum / count : null;
|
|
2801
3297
|
}
|
|
3298
|
+
/** @deprecated Use {@link GetRunAutoAvg}. */
|
|
3299
|
+
getRunAutoAvg(run) {
|
|
3300
|
+
return this.GetRunAutoAvg(run);
|
|
3301
|
+
}
|
|
2802
3302
|
/**
|
|
2803
3303
|
* Get count of tests with auto scores for a run
|
|
2804
3304
|
*/
|
|
2805
|
-
|
|
3305
|
+
GetRunAutoCount(run) {
|
|
2806
3306
|
let count = 0;
|
|
2807
3307
|
run.testResults.forEach(result => {
|
|
2808
3308
|
if (result.score != null)
|
|
@@ -2810,21 +3310,33 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2810
3310
|
});
|
|
2811
3311
|
return count;
|
|
2812
3312
|
}
|
|
3313
|
+
/** @deprecated Use {@link GetRunAutoCount}. */
|
|
3314
|
+
getRunAutoCount(run) {
|
|
3315
|
+
return this.GetRunAutoCount(run);
|
|
3316
|
+
}
|
|
2813
3317
|
/**
|
|
2814
3318
|
* Navigate to a test run when clicking a matrix cell
|
|
2815
3319
|
*/
|
|
2816
|
-
|
|
3320
|
+
OpenTestRun(testRunId) {
|
|
2817
3321
|
SharedService.Instance.OpenEntityRecord('MJ: Test Runs', CompositeKey.FromID(testRunId));
|
|
2818
3322
|
}
|
|
3323
|
+
/** @deprecated Use {@link OpenTestRun}. */
|
|
3324
|
+
openTestRun(testRunId) {
|
|
3325
|
+
return this.OpenTestRun(testRunId);
|
|
3326
|
+
}
|
|
2819
3327
|
/**
|
|
2820
3328
|
* Handle matrix cell click - navigate to the test run
|
|
2821
3329
|
*/
|
|
2822
|
-
|
|
3330
|
+
OnMatrixCellClick(result, event) {
|
|
2823
3331
|
event.stopPropagation(); // Prevent row click from also firing
|
|
2824
3332
|
if (result?.testRunId) {
|
|
2825
|
-
this.
|
|
3333
|
+
this.OpenTestRun(result.testRunId);
|
|
2826
3334
|
}
|
|
2827
3335
|
}
|
|
3336
|
+
/** @deprecated Use {@link OnMatrixCellClick}. */
|
|
3337
|
+
onMatrixCellClick(result, event) {
|
|
3338
|
+
return this.OnMatrixCellClick(result, event);
|
|
3339
|
+
}
|
|
2828
3340
|
// ===========================
|
|
2829
3341
|
// D3 Chart Rendering
|
|
2830
3342
|
// ===========================
|
|
@@ -2833,15 +3345,15 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2833
3345
|
* with trend lines and better visual hierarchy
|
|
2834
3346
|
*/
|
|
2835
3347
|
renderChart() {
|
|
2836
|
-
if (!this.
|
|
3348
|
+
if (!this.ChartContainer?.nativeElement || this.MatrixData.length === 0) {
|
|
2837
3349
|
return;
|
|
2838
3350
|
}
|
|
2839
|
-
const container = this.
|
|
2840
|
-
const tests = this.
|
|
2841
|
-
const runs = this.
|
|
3351
|
+
const container = this.ChartContainer.nativeElement;
|
|
3352
|
+
const tests = this.GetUniqueTestsFromMatrix();
|
|
3353
|
+
const runs = this.MatrixData;
|
|
2842
3354
|
// Dynamic sizing based on content and evaluation preferences
|
|
2843
3355
|
const width = container.clientWidth || 900;
|
|
2844
|
-
const evalCount = this.
|
|
3356
|
+
const evalCount = this.GetEvalCount();
|
|
2845
3357
|
// Adjust row height and column width based on how many eval types are shown
|
|
2846
3358
|
const rowHeight = evalCount >= 3 ? 34 : evalCount === 2 ? 30 : 28;
|
|
2847
3359
|
const minColWidth = evalCount >= 3 ? 65 : evalCount === 2 ? 55 : 50;
|
|
@@ -2918,9 +3430,9 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2918
3430
|
.attr('fill', '#475569')
|
|
2919
3431
|
.attr('font-size', '10px')
|
|
2920
3432
|
.attr('font-weight', '500')
|
|
2921
|
-
.text(this.
|
|
3433
|
+
.text(this.GetRelativeTime(run.date))
|
|
2922
3434
|
.style('cursor', 'pointer')
|
|
2923
|
-
.on('click', () => this.
|
|
3435
|
+
.on('click', () => this.OpenSuiteRun(run.runId));
|
|
2924
3436
|
// Pass rate badge
|
|
2925
3437
|
const passRateColor = run.passRate >= 80 ? '#22c55e' : run.passRate >= 50 ? '#f97316' : '#ef4444';
|
|
2926
3438
|
const badgeWidth = 36;
|
|
@@ -2963,7 +3475,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2963
3475
|
.attr('stroke', '#93c5fd')
|
|
2964
3476
|
.attr('stroke-width', 1)
|
|
2965
3477
|
.style('cursor', 'pointer')
|
|
2966
|
-
.on('click', () => this.
|
|
3478
|
+
.on('click', () => this.OpenSuiteRun(run.runId));
|
|
2967
3479
|
// Tag text
|
|
2968
3480
|
chart.append('text')
|
|
2969
3481
|
.attr('x', tagX + tagPillWidth / 2)
|
|
@@ -2974,7 +3486,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
2974
3486
|
.attr('font-weight', '600')
|
|
2975
3487
|
.text(tag.length > 8 ? tag.substring(0, 7) + '…' : tag)
|
|
2976
3488
|
.style('cursor', 'pointer')
|
|
2977
|
-
.on('click', () => this.
|
|
3489
|
+
.on('click', () => this.OpenSuiteRun(run.runId));
|
|
2978
3490
|
});
|
|
2979
3491
|
// Show +N indicator if more tags
|
|
2980
3492
|
if (run.tags.length > 2) {
|
|
@@ -3061,20 +3573,20 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3061
3573
|
}
|
|
3062
3574
|
// Build tooltip content based on evaluation preferences
|
|
3063
3575
|
const tooltipParts = [];
|
|
3064
|
-
if (this.
|
|
3576
|
+
if (this.EvalPreferences.showExecution) {
|
|
3065
3577
|
tooltipParts.push(`<div style="display:inline-block; padding:2px 8px; border-radius:4px; background:${statusColors[result.status]}; color:white; font-size:11px; font-weight:600">${result.status}</div>`);
|
|
3066
3578
|
}
|
|
3067
|
-
if (this.
|
|
3579
|
+
if (this.EvalPreferences.showHuman) {
|
|
3068
3580
|
tooltipParts.push(`<div style="margin-top:4px"><span style="color:var(--mj-status-warning)">👤</span> <strong>Human:</strong> <span style="color:var(--mj-text-disabled)">Needs review</span></div>`);
|
|
3069
3581
|
}
|
|
3070
|
-
if (this.
|
|
3582
|
+
if (this.EvalPreferences.showAuto && result.score != null) {
|
|
3071
3583
|
tooltipParts.push(`<div style="margin-top:4px"><span style="color:var(--mj-status-info)">🤖</span> <strong>Auto:</strong> ${(result.score * 100).toFixed(1)}%</div>`);
|
|
3072
3584
|
}
|
|
3073
3585
|
const durationText = result.duration != null ? `<div><strong>Duration:</strong> ${result.duration.toFixed(2)}s</div>` : '';
|
|
3074
3586
|
const cellGroup = chart.append('g')
|
|
3075
3587
|
.attr('class', 'result-cell')
|
|
3076
3588
|
.style('cursor', 'pointer')
|
|
3077
|
-
.on('click', () => this.
|
|
3589
|
+
.on('click', () => this.OpenTestRun(result.testRunId))
|
|
3078
3590
|
.on('mouseover', (event) => {
|
|
3079
3591
|
d3.select(event.currentTarget).select('rect.cell-bg')
|
|
3080
3592
|
.attr('stroke', '#1e40af')
|
|
@@ -3085,7 +3597,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3085
3597
|
<div style="font-weight:600; margin-bottom:6px; color:#f1f5f9">${result.testName}</div>
|
|
3086
3598
|
${tooltipParts.join('')}
|
|
3087
3599
|
${durationText}
|
|
3088
|
-
<div style="margin-top:6px; color:#94a3b8; font-size:10px">${this.
|
|
3600
|
+
<div style="margin-top:6px; color:#94a3b8; font-size:10px">${this.GetRelativeTime(run.date)} • Click to view</div>
|
|
3089
3601
|
`)
|
|
3090
3602
|
.style('left', `${event.offsetX + 15}px`)
|
|
3091
3603
|
.style('top', `${event.offsetY - 10}px`);
|
|
@@ -3099,11 +3611,11 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3099
3611
|
// Determine cell background color based on eval preferences
|
|
3100
3612
|
let cellBgColor = '#f1f5f9';
|
|
3101
3613
|
let cellBgGradient = '';
|
|
3102
|
-
if (this.
|
|
3614
|
+
if (this.EvalPreferences.showExecution) {
|
|
3103
3615
|
// Use status-based gradient
|
|
3104
3616
|
cellBgGradient = `url(#gradient-${result.status.toLowerCase()})`;
|
|
3105
3617
|
}
|
|
3106
|
-
else if (this.
|
|
3618
|
+
else if (this.EvalPreferences.showAuto && result.score != null) {
|
|
3107
3619
|
// Use score-based color
|
|
3108
3620
|
const scoreColor = result.score >= 0.8 ? '#22c55e' : result.score >= 0.5 ? '#f97316' : '#ef4444';
|
|
3109
3621
|
cellBgColor = scoreColor;
|
|
@@ -3131,7 +3643,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3131
3643
|
const startX = x + cellWidth / 2 - ((evalCount - 1) * iconSpacing) / 2;
|
|
3132
3644
|
let iconIndex = 0;
|
|
3133
3645
|
// Status icon (execution)
|
|
3134
|
-
if (this.
|
|
3646
|
+
if (this.EvalPreferences.showExecution) {
|
|
3135
3647
|
const iconText = {
|
|
3136
3648
|
'Passed': '✓',
|
|
3137
3649
|
'Failed': '✕',
|
|
@@ -3162,7 +3674,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3162
3674
|
iconIndex++;
|
|
3163
3675
|
}
|
|
3164
3676
|
// Human icon
|
|
3165
|
-
if (this.
|
|
3677
|
+
if (this.EvalPreferences.showHuman) {
|
|
3166
3678
|
const iconX = startX + iconIndex * iconSpacing;
|
|
3167
3679
|
// Human evaluation indicator (clock icon for pending)
|
|
3168
3680
|
cellGroup.append('circle')
|
|
@@ -3183,7 +3695,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3183
3695
|
iconIndex++;
|
|
3184
3696
|
}
|
|
3185
3697
|
// Auto score icon/indicator
|
|
3186
|
-
if (this.
|
|
3698
|
+
if (this.EvalPreferences.showAuto) {
|
|
3187
3699
|
const iconX = startX + iconIndex * iconSpacing;
|
|
3188
3700
|
if (result.score != null) {
|
|
3189
3701
|
const scorePercent = Math.round(result.score * 100);
|
|
@@ -3315,7 +3827,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3315
3827
|
.text('SHOWING:');
|
|
3316
3828
|
let xOffset = 55;
|
|
3317
3829
|
// Status indicator
|
|
3318
|
-
if (this.
|
|
3830
|
+
if (this.EvalPreferences.showExecution) {
|
|
3319
3831
|
const statusGroup = legendGroup.append('g')
|
|
3320
3832
|
.attr('transform', `translate(${xOffset}, -4)`);
|
|
3321
3833
|
statusGroup.append('circle')
|
|
@@ -3341,7 +3853,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3341
3853
|
xOffset += 60;
|
|
3342
3854
|
}
|
|
3343
3855
|
// Human indicator
|
|
3344
|
-
if (this.
|
|
3856
|
+
if (this.EvalPreferences.showHuman) {
|
|
3345
3857
|
const humanGroup = legendGroup.append('g')
|
|
3346
3858
|
.attr('transform', `translate(${xOffset}, -4)`);
|
|
3347
3859
|
humanGroup.append('circle')
|
|
@@ -3368,7 +3880,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3368
3880
|
xOffset += 60;
|
|
3369
3881
|
}
|
|
3370
3882
|
// Auto indicator
|
|
3371
|
-
if (this.
|
|
3883
|
+
if (this.EvalPreferences.showAuto) {
|
|
3372
3884
|
const autoGroup = legendGroup.append('g')
|
|
3373
3885
|
.attr('transform', `translate(${xOffset}, -4)`);
|
|
3374
3886
|
autoGroup.append('rect')
|
|
@@ -3395,26 +3907,42 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3395
3907
|
.text('Auto');
|
|
3396
3908
|
}
|
|
3397
3909
|
}
|
|
3398
|
-
|
|
3399
|
-
const data = this.
|
|
3910
|
+
GetAveragePassRate() {
|
|
3911
|
+
const data = this.GetFilteredAnalyticsData();
|
|
3400
3912
|
if (data.length === 0)
|
|
3401
3913
|
return 0;
|
|
3402
3914
|
return data.reduce((sum, d) => sum + d.passRate, 0) / data.length;
|
|
3403
3915
|
}
|
|
3916
|
+
/** @deprecated Use {@link GetAveragePassRate}. */
|
|
3917
|
+
getAveragePassRate() {
|
|
3918
|
+
return this.GetAveragePassRate();
|
|
3919
|
+
}
|
|
3920
|
+
GetTotalRuns() {
|
|
3921
|
+
return this.GetFilteredAnalyticsData().length;
|
|
3922
|
+
}
|
|
3923
|
+
/** @deprecated Use {@link GetTotalRuns}. */
|
|
3404
3924
|
getTotalRuns() {
|
|
3405
|
-
return this.
|
|
3925
|
+
return this.GetTotalRuns();
|
|
3406
3926
|
}
|
|
3407
|
-
|
|
3408
|
-
const data = this.
|
|
3927
|
+
GetAverageDuration() {
|
|
3928
|
+
const data = this.GetFilteredAnalyticsData();
|
|
3409
3929
|
if (data.length === 0)
|
|
3410
3930
|
return 0;
|
|
3411
3931
|
return data.reduce((sum, d) => sum + d.duration, 0) / data.length;
|
|
3412
3932
|
}
|
|
3933
|
+
/** @deprecated Use {@link GetAverageDuration}. */
|
|
3934
|
+
getAverageDuration() {
|
|
3935
|
+
return this.GetAverageDuration();
|
|
3936
|
+
}
|
|
3937
|
+
GetTotalCost() {
|
|
3938
|
+
return this.GetFilteredAnalyticsData().reduce((sum, d) => sum + d.cost, 0);
|
|
3939
|
+
}
|
|
3940
|
+
/** @deprecated Use {@link GetTotalCost}. */
|
|
3413
3941
|
getTotalCost() {
|
|
3414
|
-
return this.
|
|
3942
|
+
return this.GetTotalCost();
|
|
3415
3943
|
}
|
|
3416
|
-
|
|
3417
|
-
const data = this.
|
|
3944
|
+
GetPassRateTrend() {
|
|
3945
|
+
const data = this.GetFilteredAnalyticsData();
|
|
3418
3946
|
if (data.length < 2)
|
|
3419
3947
|
return { direction: 'stable', value: 0 };
|
|
3420
3948
|
// Compare recent half to older half
|
|
@@ -3428,6 +3956,10 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3428
3956
|
return { direction: 'stable', value: diff };
|
|
3429
3957
|
return { direction: diff > 0 ? 'up' : 'down', value: Math.abs(diff) };
|
|
3430
3958
|
}
|
|
3959
|
+
/** @deprecated Use {@link GetPassRateTrend}. */
|
|
3960
|
+
getPassRateTrend() {
|
|
3961
|
+
return this.GetPassRateTrend();
|
|
3962
|
+
}
|
|
3431
3963
|
formatDuration(seconds) {
|
|
3432
3964
|
if (seconds < 60)
|
|
3433
3965
|
return `${seconds.toFixed(1)}s`;
|
|
@@ -3440,39 +3972,55 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3440
3972
|
const mins = Math.floor((seconds % 3600) / 60);
|
|
3441
3973
|
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
3442
3974
|
}
|
|
3443
|
-
|
|
3975
|
+
FormatCost(cost) {
|
|
3444
3976
|
if (cost < 0.01)
|
|
3445
3977
|
return `$${cost.toFixed(6)}`;
|
|
3446
3978
|
if (cost < 1)
|
|
3447
3979
|
return `$${cost.toFixed(4)}`;
|
|
3448
3980
|
return `$${cost.toFixed(2)}`;
|
|
3449
3981
|
}
|
|
3982
|
+
/** @deprecated Use {@link FormatCost}. */
|
|
3983
|
+
formatCost(cost) {
|
|
3984
|
+
return this.FormatCost(cost);
|
|
3985
|
+
}
|
|
3450
3986
|
// ==========================================
|
|
3451
3987
|
// Compare Tab Methods
|
|
3452
3988
|
// ==========================================
|
|
3989
|
+
async SelectCompareRunA(run) {
|
|
3990
|
+
this.CompareRunA = run;
|
|
3991
|
+
await this.loadCompareData();
|
|
3992
|
+
}
|
|
3993
|
+
/** @deprecated Use {@link SelectCompareRunA}. */
|
|
3453
3994
|
async selectCompareRunA(run) {
|
|
3454
|
-
this.
|
|
3995
|
+
return this.SelectCompareRunA(run);
|
|
3996
|
+
}
|
|
3997
|
+
async SelectCompareRunB(run) {
|
|
3998
|
+
this.CompareRunB = run;
|
|
3455
3999
|
await this.loadCompareData();
|
|
3456
4000
|
}
|
|
4001
|
+
/** @deprecated Use {@link SelectCompareRunB}. */
|
|
3457
4002
|
async selectCompareRunB(run) {
|
|
3458
|
-
this.
|
|
3459
|
-
await this.loadCompareData();
|
|
4003
|
+
return this.SelectCompareRunB(run);
|
|
3460
4004
|
}
|
|
3461
|
-
|
|
3462
|
-
this.
|
|
3463
|
-
this.
|
|
3464
|
-
this.
|
|
3465
|
-
this.
|
|
3466
|
-
this.
|
|
4005
|
+
ClearCompareSelection() {
|
|
4006
|
+
this.CompareRunA = null;
|
|
4007
|
+
this.CompareRunB = null;
|
|
4008
|
+
this.CompareResults = [];
|
|
4009
|
+
this.CompareRunATests = [];
|
|
4010
|
+
this.CompareRunBTests = [];
|
|
3467
4011
|
this.cdr.markForCheck();
|
|
3468
4012
|
}
|
|
4013
|
+
/** @deprecated Use {@link ClearCompareSelection}. */
|
|
4014
|
+
clearCompareSelection() {
|
|
4015
|
+
return this.ClearCompareSelection();
|
|
4016
|
+
}
|
|
3469
4017
|
async loadCompareData() {
|
|
3470
|
-
if (!this.
|
|
3471
|
-
this.
|
|
4018
|
+
if (!this.CompareRunA || !this.CompareRunB) {
|
|
4019
|
+
this.CompareResults = [];
|
|
3472
4020
|
this.cdr.markForCheck();
|
|
3473
4021
|
return;
|
|
3474
4022
|
}
|
|
3475
|
-
this.
|
|
4023
|
+
this.LoadingCompare = true;
|
|
3476
4024
|
this.cdr.markForCheck();
|
|
3477
4025
|
try {
|
|
3478
4026
|
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
@@ -3480,28 +4028,28 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3480
4028
|
const [resultA, resultB] = await Promise.all([
|
|
3481
4029
|
rv.RunView({
|
|
3482
4030
|
EntityName: 'MJ: Test Runs',
|
|
3483
|
-
ExtraFilter: `TestSuiteRunID='${this.
|
|
4031
|
+
ExtraFilter: `TestSuiteRunID='${this.CompareRunA.ID}'`,
|
|
3484
4032
|
OrderBy: 'Sequence ASC',
|
|
3485
4033
|
ResultType: 'entity_object'
|
|
3486
4034
|
}),
|
|
3487
4035
|
rv.RunView({
|
|
3488
4036
|
EntityName: 'MJ: Test Runs',
|
|
3489
|
-
ExtraFilter: `TestSuiteRunID='${this.
|
|
4037
|
+
ExtraFilter: `TestSuiteRunID='${this.CompareRunB.ID}'`,
|
|
3490
4038
|
OrderBy: 'Sequence ASC',
|
|
3491
4039
|
ResultType: 'entity_object'
|
|
3492
4040
|
})
|
|
3493
4041
|
]);
|
|
3494
|
-
this.
|
|
3495
|
-
this.
|
|
4042
|
+
this.CompareRunATests = resultA.Success ? resultA.Results || [] : [];
|
|
4043
|
+
this.CompareRunBTests = resultB.Success ? resultB.Results || [] : [];
|
|
3496
4044
|
// Build comparison results
|
|
3497
|
-
this.
|
|
4045
|
+
this.CompareResults = this.buildComparisonResults(this.CompareRunATests, this.CompareRunBTests);
|
|
3498
4046
|
}
|
|
3499
4047
|
catch (error) {
|
|
3500
4048
|
console.error('Error loading comparison data:', error);
|
|
3501
4049
|
SharedService.Instance.CreateSimpleNotification('Failed to load comparison data', 'error', 3000);
|
|
3502
4050
|
}
|
|
3503
4051
|
finally {
|
|
3504
|
-
this.
|
|
4052
|
+
this.LoadingCompare = false;
|
|
3505
4053
|
this.cdr.markForCheck();
|
|
3506
4054
|
}
|
|
3507
4055
|
}
|
|
@@ -3547,48 +4095,72 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3547
4095
|
}
|
|
3548
4096
|
return results;
|
|
3549
4097
|
}
|
|
3550
|
-
|
|
3551
|
-
if (!this.
|
|
4098
|
+
GetComparePassRateDiff() {
|
|
4099
|
+
if (!this.CompareRunA || !this.CompareRunB)
|
|
3552
4100
|
return null;
|
|
3553
|
-
const rateA = this.
|
|
3554
|
-
const rateB = this.
|
|
4101
|
+
const rateA = this.GetPassRate(this.CompareRunA);
|
|
4102
|
+
const rateB = this.GetPassRate(this.CompareRunB);
|
|
3555
4103
|
return rateB - rateA;
|
|
3556
4104
|
}
|
|
3557
|
-
|
|
3558
|
-
|
|
4105
|
+
/** @deprecated Use {@link GetComparePassRateDiff}. */
|
|
4106
|
+
getComparePassRateDiff() {
|
|
4107
|
+
return this.GetComparePassRateDiff();
|
|
4108
|
+
}
|
|
4109
|
+
GetCompareDurationDiff() {
|
|
4110
|
+
if (!this.CompareRunA || !this.CompareRunB)
|
|
3559
4111
|
return null;
|
|
3560
|
-
const durA = this.
|
|
3561
|
-
const durB = this.
|
|
4112
|
+
const durA = this.CompareRunA.TotalDurationSeconds || 0;
|
|
4113
|
+
const durB = this.CompareRunB.TotalDurationSeconds || 0;
|
|
3562
4114
|
return durB - durA;
|
|
3563
4115
|
}
|
|
3564
|
-
|
|
3565
|
-
|
|
4116
|
+
/** @deprecated Use {@link GetCompareDurationDiff}. */
|
|
4117
|
+
getCompareDurationDiff() {
|
|
4118
|
+
return this.GetCompareDurationDiff();
|
|
4119
|
+
}
|
|
4120
|
+
GetAbsCompareDurationDiff() {
|
|
4121
|
+
const diff = this.GetCompareDurationDiff();
|
|
3566
4122
|
return diff != null ? Math.abs(diff) : 0;
|
|
3567
4123
|
}
|
|
3568
|
-
|
|
3569
|
-
|
|
4124
|
+
/** @deprecated Use {@link GetAbsCompareDurationDiff}. */
|
|
4125
|
+
getAbsCompareDurationDiff() {
|
|
4126
|
+
return this.GetAbsCompareDurationDiff();
|
|
4127
|
+
}
|
|
4128
|
+
GetCompareCostDiff() {
|
|
4129
|
+
if (!this.CompareRunA || !this.CompareRunB)
|
|
3570
4130
|
return null;
|
|
3571
|
-
const costA = this.
|
|
3572
|
-
const costB = this.
|
|
4131
|
+
const costA = this.CompareRunA.TotalCostUSD || 0;
|
|
4132
|
+
const costB = this.CompareRunB.TotalCostUSD || 0;
|
|
3573
4133
|
return costB - costA;
|
|
3574
4134
|
}
|
|
3575
|
-
|
|
3576
|
-
|
|
4135
|
+
/** @deprecated Use {@link GetCompareCostDiff}. */
|
|
4136
|
+
getCompareCostDiff() {
|
|
4137
|
+
return this.GetCompareCostDiff();
|
|
4138
|
+
}
|
|
4139
|
+
GetCompareImprovedCount() {
|
|
4140
|
+
return this.CompareResults.filter(r => r.runA && r.runB &&
|
|
3577
4141
|
r.runA.status !== 'Passed' && r.runB.status === 'Passed').length;
|
|
3578
4142
|
}
|
|
3579
|
-
|
|
3580
|
-
|
|
4143
|
+
/** @deprecated Use {@link GetCompareImprovedCount}. */
|
|
4144
|
+
getCompareImprovedCount() {
|
|
4145
|
+
return this.GetCompareImprovedCount();
|
|
4146
|
+
}
|
|
4147
|
+
GetCompareRegressedCount() {
|
|
4148
|
+
return this.CompareResults.filter(r => r.runA && r.runB &&
|
|
3581
4149
|
r.runA.status === 'Passed' && r.runB.status !== 'Passed').length;
|
|
3582
4150
|
}
|
|
4151
|
+
/** @deprecated Use {@link GetCompareRegressedCount}. */
|
|
4152
|
+
getCompareRegressedCount() {
|
|
4153
|
+
return this.GetCompareRegressedCount();
|
|
4154
|
+
}
|
|
3583
4155
|
// ==========================================
|
|
3584
4156
|
// Excel Export Methods
|
|
3585
4157
|
// ==========================================
|
|
3586
|
-
async
|
|
4158
|
+
async ExportToExcel() {
|
|
3587
4159
|
try {
|
|
3588
4160
|
// Ensure runs are loaded
|
|
3589
|
-
if (!this.
|
|
4161
|
+
if (!this.RunsLoaded)
|
|
3590
4162
|
await this.loadRuns();
|
|
3591
|
-
const data = this.
|
|
4163
|
+
const data = this.SuiteRuns.map(run => ({
|
|
3592
4164
|
'Run ID': run.ID,
|
|
3593
4165
|
'Status': run.Status,
|
|
3594
4166
|
'Started At': run.StartedAt ? new Date(run.StartedAt).toLocaleString() : 'N/A',
|
|
@@ -3614,6 +4186,10 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3614
4186
|
SharedService.Instance.CreateSimpleNotification('Export failed', 'error', 3000);
|
|
3615
4187
|
}
|
|
3616
4188
|
}
|
|
4189
|
+
/** @deprecated Use {@link ExportToExcel}. */
|
|
4190
|
+
async exportToExcel() {
|
|
4191
|
+
return this.ExportToExcel();
|
|
4192
|
+
}
|
|
3617
4193
|
downloadAsCSV(data, filename) {
|
|
3618
4194
|
if (data.length === 0)
|
|
3619
4195
|
return;
|
|
@@ -3637,20 +4213,24 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3637
4213
|
link.click();
|
|
3638
4214
|
URL.revokeObjectURL(link.href);
|
|
3639
4215
|
}
|
|
3640
|
-
|
|
4216
|
+
GetRunTags(run) {
|
|
3641
4217
|
return TagsHelper.parseTags(run.Tags);
|
|
3642
4218
|
}
|
|
4219
|
+
/** @deprecated Use {@link GetRunTags}. */
|
|
4220
|
+
getRunTags(run) {
|
|
4221
|
+
return this.GetRunTags(run);
|
|
4222
|
+
}
|
|
3643
4223
|
/**
|
|
3644
4224
|
* Export matrix view data to CSV
|
|
3645
4225
|
*/
|
|
3646
|
-
|
|
3647
|
-
if (this.
|
|
4226
|
+
ExportMatrixToCSV() {
|
|
4227
|
+
if (this.MatrixData.length === 0) {
|
|
3648
4228
|
SharedService.Instance.CreateSimpleNotification('No data to export', 'warning', 2000);
|
|
3649
4229
|
return;
|
|
3650
4230
|
}
|
|
3651
4231
|
try {
|
|
3652
|
-
const tests = this.
|
|
3653
|
-
const runs = this.
|
|
4232
|
+
const tests = this.GetUniqueTestsFromMatrix();
|
|
4233
|
+
const runs = this.MatrixData;
|
|
3654
4234
|
// Build CSV rows
|
|
3655
4235
|
const rows = [];
|
|
3656
4236
|
for (const test of tests) {
|
|
@@ -3662,16 +4242,16 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3662
4242
|
for (const run of runs) {
|
|
3663
4243
|
const result = run.testResults.get(test.testId);
|
|
3664
4244
|
const runLabel = run.tags.length > 0
|
|
3665
|
-
? `${run.tags.slice(0, 2).join('/')} (${this.
|
|
3666
|
-
: this.
|
|
4245
|
+
? `${run.tags.slice(0, 2).join('/')} (${this.GetRelativeTime(run.date)})`
|
|
4246
|
+
: this.GetRelativeTime(run.date);
|
|
3667
4247
|
if (result) {
|
|
3668
4248
|
// Build cell value based on what's shown
|
|
3669
4249
|
const parts = [];
|
|
3670
|
-
if (this.
|
|
4250
|
+
if (this.EvalPreferences.showExecution)
|
|
3671
4251
|
parts.push(result.status);
|
|
3672
|
-
if (this.
|
|
4252
|
+
if (this.EvalPreferences.showHuman)
|
|
3673
4253
|
parts.push(result.humanRating != null ? `H:${result.humanRating}` : 'H:-');
|
|
3674
|
-
if (this.
|
|
4254
|
+
if (this.EvalPreferences.showAuto)
|
|
3675
4255
|
parts.push(result.score != null ? `A:${Math.round(result.score * 100)}%` : 'A:-');
|
|
3676
4256
|
row[runLabel] = parts.join(' | ');
|
|
3677
4257
|
}
|
|
@@ -3689,6 +4269,10 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3689
4269
|
SharedService.Instance.CreateSimpleNotification('Export failed', 'error', 3000);
|
|
3690
4270
|
}
|
|
3691
4271
|
}
|
|
4272
|
+
/** @deprecated Use {@link ExportMatrixToCSV}. */
|
|
4273
|
+
exportMatrixToCSV() {
|
|
4274
|
+
return this.ExportMatrixToCSV();
|
|
4275
|
+
}
|
|
3692
4276
|
// ==========================================
|
|
3693
4277
|
// Keyboard Shortcuts Settings
|
|
3694
4278
|
// ==========================================
|
|
@@ -3701,11 +4285,11 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3701
4285
|
const setting = engine.UserSettings.find(s => s.Setting === SHORTCUTS_SETTINGS_KEY);
|
|
3702
4286
|
if (setting) {
|
|
3703
4287
|
this.shortcutsSettingEntity = setting;
|
|
3704
|
-
this.
|
|
4288
|
+
this.ShowShortcuts = setting.Value === 'true';
|
|
3705
4289
|
}
|
|
3706
4290
|
else {
|
|
3707
4291
|
// Default to hidden
|
|
3708
|
-
this.
|
|
4292
|
+
this.ShowShortcuts = false;
|
|
3709
4293
|
}
|
|
3710
4294
|
this.cdr.markForCheck();
|
|
3711
4295
|
}
|
|
@@ -3716,24 +4300,32 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3716
4300
|
/**
|
|
3717
4301
|
* Toggle analytics filters visibility
|
|
3718
4302
|
*/
|
|
3719
|
-
|
|
3720
|
-
this.
|
|
4303
|
+
ToggleFilters() {
|
|
4304
|
+
this.FiltersCollapsed = !this.FiltersCollapsed;
|
|
3721
4305
|
this.cdr.markForCheck();
|
|
3722
4306
|
}
|
|
4307
|
+
/** @deprecated Use {@link ToggleFilters}. */
|
|
4308
|
+
toggleFilters() {
|
|
4309
|
+
return this.ToggleFilters();
|
|
4310
|
+
}
|
|
3723
4311
|
/**
|
|
3724
4312
|
* Applies the expanded/collapsed state emitted by the filters mj-accordion-panel.
|
|
3725
4313
|
* The panel's Expanded is the inverse of filtersCollapsed; set (not flip) so it
|
|
3726
4314
|
* stays in sync, and mark for check (OnPush change detection).
|
|
3727
4315
|
*/
|
|
3728
|
-
|
|
3729
|
-
this.
|
|
4316
|
+
OnFiltersExpandedChange(expanded) {
|
|
4317
|
+
this.FiltersCollapsed = !expanded;
|
|
3730
4318
|
this.cdr.markForCheck();
|
|
3731
4319
|
}
|
|
4320
|
+
/** @deprecated Use {@link OnFiltersExpandedChange}. */
|
|
4321
|
+
onFiltersExpandedChange(expanded) {
|
|
4322
|
+
return this.OnFiltersExpandedChange(expanded);
|
|
4323
|
+
}
|
|
3732
4324
|
/**
|
|
3733
4325
|
* Toggle keyboard shortcuts visibility and save preference
|
|
3734
4326
|
*/
|
|
3735
|
-
async
|
|
3736
|
-
this.
|
|
4327
|
+
async ToggleShortcuts() {
|
|
4328
|
+
this.ShowShortcuts = !this.ShowShortcuts;
|
|
3737
4329
|
this.cdr.markForCheck();
|
|
3738
4330
|
try {
|
|
3739
4331
|
const userId = this.metadata.CurrentUser?.ID;
|
|
@@ -3751,34 +4343,46 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3751
4343
|
this.shortcutsSettingEntity.Setting = SHORTCUTS_SETTINGS_KEY;
|
|
3752
4344
|
}
|
|
3753
4345
|
}
|
|
3754
|
-
this.shortcutsSettingEntity.Value = this.
|
|
4346
|
+
this.shortcutsSettingEntity.Value = this.ShowShortcuts ? 'true' : 'false';
|
|
3755
4347
|
await this.shortcutsSettingEntity.Save();
|
|
3756
4348
|
}
|
|
3757
4349
|
catch (error) {
|
|
3758
4350
|
console.warn('Failed to save shortcuts setting:', error);
|
|
3759
4351
|
}
|
|
3760
4352
|
}
|
|
4353
|
+
/** @deprecated Use {@link ToggleShortcuts}. */
|
|
4354
|
+
async toggleShortcuts() {
|
|
4355
|
+
return this.ToggleShortcuts();
|
|
4356
|
+
}
|
|
3761
4357
|
/** Case-insensitive UUID check whether a run is the currently selected Run A (baseline). */
|
|
3762
4358
|
IsCompareRunA(run) {
|
|
3763
|
-
return UUIDsEqual(this.
|
|
4359
|
+
return UUIDsEqual(this.CompareRunA?.ID, run.ID);
|
|
3764
4360
|
}
|
|
3765
4361
|
/** Case-insensitive UUID check whether a run is the currently selected Run B (compare). */
|
|
3766
4362
|
IsCompareRunB(run) {
|
|
3767
|
-
return UUIDsEqual(this.
|
|
4363
|
+
return UUIDsEqual(this.CompareRunB?.ID, run.ID);
|
|
3768
4364
|
}
|
|
3769
4365
|
// ==========================================
|
|
3770
4366
|
// Edit / Save Methods
|
|
3771
4367
|
// ==========================================
|
|
3772
4368
|
/** True if the suite record has any unsaved field changes. */
|
|
3773
|
-
get
|
|
4369
|
+
get IsDirty() {
|
|
3774
4370
|
return this.record?.Dirty === true;
|
|
3775
4371
|
}
|
|
4372
|
+
/** @deprecated Use {@link IsDirty}. */
|
|
4373
|
+
get isDirty() {
|
|
4374
|
+
return this.IsDirty;
|
|
4375
|
+
}
|
|
3776
4376
|
/** Names of fields with pending edits, used by the save bar. */
|
|
3777
|
-
get
|
|
4377
|
+
get DirtyFieldNames() {
|
|
3778
4378
|
if (!this.record?.Fields)
|
|
3779
4379
|
return [];
|
|
3780
4380
|
return this.record.Fields.filter(f => f.Dirty).map(f => f.Name);
|
|
3781
4381
|
}
|
|
4382
|
+
/** @deprecated Use {@link DirtyFieldNames}. */
|
|
4383
|
+
get dirtyFieldNames() {
|
|
4384
|
+
return this.DirtyFieldNames;
|
|
4385
|
+
}
|
|
3782
4386
|
/** Parsed tags as a plain array — derived from record.Tags JSON. */
|
|
3783
4387
|
get tags() {
|
|
3784
4388
|
return TagsHelper.parseTags(this.record?.Tags);
|
|
@@ -3797,7 +4401,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3797
4401
|
ResultType: 'entity_object'
|
|
3798
4402
|
});
|
|
3799
4403
|
if (result.Success) {
|
|
3800
|
-
this.
|
|
4404
|
+
this.ParentSuiteOptions = result.Results || [];
|
|
3801
4405
|
this.cdr.markForCheck();
|
|
3802
4406
|
}
|
|
3803
4407
|
}
|
|
@@ -3806,10 +4410,10 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3806
4410
|
}
|
|
3807
4411
|
}
|
|
3808
4412
|
/** Save all pending changes on the suite record. */
|
|
3809
|
-
async
|
|
3810
|
-
if (!this.
|
|
4413
|
+
async SaveChanges() {
|
|
4414
|
+
if (!this.IsDirty || this.IsSaving)
|
|
3811
4415
|
return;
|
|
3812
|
-
this.
|
|
4416
|
+
this.IsSaving = true;
|
|
3813
4417
|
this.cdr.markForCheck();
|
|
3814
4418
|
try {
|
|
3815
4419
|
const ok = await this.SaveRecord(false);
|
|
@@ -3826,23 +4430,31 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3826
4430
|
SharedService.Instance.CreateSimpleNotification(msg, 'error', 4000);
|
|
3827
4431
|
}
|
|
3828
4432
|
finally {
|
|
3829
|
-
this.
|
|
4433
|
+
this.IsSaving = false;
|
|
3830
4434
|
this.cdr.markForCheck();
|
|
3831
4435
|
}
|
|
3832
4436
|
}
|
|
4437
|
+
/** @deprecated Use {@link SaveChanges}. */
|
|
4438
|
+
async saveChanges() {
|
|
4439
|
+
return this.SaveChanges();
|
|
4440
|
+
}
|
|
3833
4441
|
/** Discard all pending field changes on the suite. */
|
|
3834
|
-
async
|
|
3835
|
-
if (!this.
|
|
4442
|
+
async DiscardChanges() {
|
|
4443
|
+
if (!this.IsDirty)
|
|
3836
4444
|
return;
|
|
3837
4445
|
if (!(await this.confirmService.Confirm('Discard your unsaved changes?')))
|
|
3838
4446
|
return;
|
|
3839
4447
|
this.record.Revert();
|
|
3840
|
-
this.
|
|
4448
|
+
this.TagDraft = '';
|
|
3841
4449
|
this.cdr.markForCheck();
|
|
3842
4450
|
}
|
|
4451
|
+
/** @deprecated Use {@link DiscardChanges}. */
|
|
4452
|
+
async discardChanges() {
|
|
4453
|
+
return this.DiscardChanges();
|
|
4454
|
+
}
|
|
3843
4455
|
/** Add a tag from tagDraft (called on Enter / comma / blur). */
|
|
3844
|
-
|
|
3845
|
-
const raw = this.
|
|
4456
|
+
AddTagFromDraft() {
|
|
4457
|
+
const raw = this.TagDraft.trim();
|
|
3846
4458
|
if (!raw)
|
|
3847
4459
|
return;
|
|
3848
4460
|
// Allow comma-separated entry: "alpha, beta" → two tags
|
|
@@ -3854,49 +4466,61 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3854
4466
|
merged.push(t);
|
|
3855
4467
|
}
|
|
3856
4468
|
this.record.Tags = TagsHelper.toJson(merged);
|
|
3857
|
-
this.
|
|
4469
|
+
this.TagDraft = '';
|
|
3858
4470
|
this.cdr.markForCheck();
|
|
3859
4471
|
}
|
|
4472
|
+
/** @deprecated Use {@link AddTagFromDraft}. */
|
|
4473
|
+
addTagFromDraft() {
|
|
4474
|
+
return this.AddTagFromDraft();
|
|
4475
|
+
}
|
|
3860
4476
|
/** Remove a single tag. */
|
|
3861
|
-
|
|
4477
|
+
RemoveTag(tag) {
|
|
3862
4478
|
this.record.Tags = TagsHelper.removeTag(this.record.Tags, tag);
|
|
3863
4479
|
this.cdr.markForCheck();
|
|
3864
4480
|
}
|
|
4481
|
+
/** @deprecated Use {@link RemoveTag}. */
|
|
4482
|
+
removeTag(tag) {
|
|
4483
|
+
return this.RemoveTag(tag);
|
|
4484
|
+
}
|
|
3865
4485
|
/** Handle Enter / comma in the tag input to commit the draft tag. */
|
|
3866
|
-
|
|
4486
|
+
OnTagInputKeydown(event) {
|
|
3867
4487
|
if (event.key === 'Enter' || event.key === ',') {
|
|
3868
4488
|
event.preventDefault();
|
|
3869
|
-
this.
|
|
4489
|
+
this.AddTagFromDraft();
|
|
3870
4490
|
return;
|
|
3871
4491
|
}
|
|
3872
4492
|
// Backspace on an empty draft pops the last chip
|
|
3873
|
-
if (event.key === 'Backspace' && !this.
|
|
4493
|
+
if (event.key === 'Backspace' && !this.TagDraft) {
|
|
3874
4494
|
const all = this.tags;
|
|
3875
4495
|
if (all.length > 0) {
|
|
3876
4496
|
event.preventDefault();
|
|
3877
|
-
this.
|
|
4497
|
+
this.RemoveTag(all[all.length - 1]);
|
|
3878
4498
|
}
|
|
3879
4499
|
}
|
|
3880
4500
|
}
|
|
4501
|
+
/** @deprecated Use {@link OnTagInputKeydown}. */
|
|
4502
|
+
onTagInputKeydown(event) {
|
|
4503
|
+
return this.OnTagInputKeydown(event);
|
|
4504
|
+
}
|
|
3881
4505
|
// ==========================================
|
|
3882
4506
|
// Suite Membership: Add / Remove / Reorder Tests
|
|
3883
4507
|
// ==========================================
|
|
3884
4508
|
/** Empty-state message shown when the add-tests search matches nothing. */
|
|
3885
4509
|
get AddTestsNoMatchMessage() {
|
|
3886
|
-
return `No tests match "${this.
|
|
4510
|
+
return `No tests match "${this.AddTestsSearch}".`;
|
|
3887
4511
|
}
|
|
3888
4512
|
/** Open the picker dialog and load tests not yet in this suite. */
|
|
3889
|
-
async
|
|
3890
|
-
this.
|
|
3891
|
-
this.
|
|
3892
|
-
this.
|
|
3893
|
-
this.
|
|
4513
|
+
async OpenAddTestsDialog() {
|
|
4514
|
+
this.ShowAddTestsDialog = true;
|
|
4515
|
+
this.SelectedTestIdsToAdd = new Set();
|
|
4516
|
+
this.AddTestsSearch = '';
|
|
4517
|
+
this.LoadingAvailableTests = true;
|
|
3894
4518
|
this.cdr.markForCheck();
|
|
3895
4519
|
try {
|
|
3896
|
-
if (!this.
|
|
4520
|
+
if (!this.TestsLoaded) {
|
|
3897
4521
|
await this.loadTests();
|
|
3898
4522
|
}
|
|
3899
|
-
const existingTestIds = new Set(this.
|
|
4523
|
+
const existingTestIds = new Set(this.SuiteTests.map(st => st.TestID));
|
|
3900
4524
|
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
3901
4525
|
const result = await rv.RunView({
|
|
3902
4526
|
EntityName: 'MJ: Tests',
|
|
@@ -3904,7 +4528,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3904
4528
|
ResultType: 'entity_object'
|
|
3905
4529
|
});
|
|
3906
4530
|
if (result.Success) {
|
|
3907
|
-
this.
|
|
4531
|
+
this.AvailableTests = (result.Results || []).filter(t => !existingTestIds.has(t.ID));
|
|
3908
4532
|
}
|
|
3909
4533
|
else {
|
|
3910
4534
|
SharedService.Instance.CreateSimpleNotification('Failed to load tests', 'error', 3000);
|
|
@@ -3915,65 +4539,93 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
3915
4539
|
SharedService.Instance.CreateSimpleNotification('Failed to load tests', 'error', 3000);
|
|
3916
4540
|
}
|
|
3917
4541
|
finally {
|
|
3918
|
-
this.
|
|
4542
|
+
this.LoadingAvailableTests = false;
|
|
3919
4543
|
this.cdr.markForCheck();
|
|
3920
4544
|
}
|
|
3921
4545
|
}
|
|
3922
|
-
|
|
3923
|
-
|
|
4546
|
+
/** @deprecated Use {@link OpenAddTestsDialog}. */
|
|
4547
|
+
async openAddTestsDialog() {
|
|
4548
|
+
return this.OpenAddTestsDialog();
|
|
4549
|
+
}
|
|
4550
|
+
CloseAddTestsDialog() {
|
|
4551
|
+
if (this.IsAddingTests)
|
|
3924
4552
|
return;
|
|
3925
|
-
this.
|
|
3926
|
-
this.
|
|
3927
|
-
this.
|
|
4553
|
+
this.ShowAddTestsDialog = false;
|
|
4554
|
+
this.SelectedTestIdsToAdd = new Set();
|
|
4555
|
+
this.AddTestsSearch = '';
|
|
3928
4556
|
this.cdr.markForCheck();
|
|
3929
4557
|
}
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
4558
|
+
/** @deprecated Use {@link CloseAddTestsDialog}. */
|
|
4559
|
+
closeAddTestsDialog() {
|
|
4560
|
+
return this.CloseAddTestsDialog();
|
|
4561
|
+
}
|
|
4562
|
+
ToggleAddSelection(testId) {
|
|
4563
|
+
if (this.SelectedTestIdsToAdd.has(testId)) {
|
|
4564
|
+
this.SelectedTestIdsToAdd.delete(testId);
|
|
3933
4565
|
}
|
|
3934
4566
|
else {
|
|
3935
|
-
this.
|
|
4567
|
+
this.SelectedTestIdsToAdd.add(testId);
|
|
3936
4568
|
}
|
|
3937
4569
|
// Set mutation alone doesn't trigger OnPush — assign a new reference
|
|
3938
|
-
this.
|
|
4570
|
+
this.SelectedTestIdsToAdd = new Set(this.SelectedTestIdsToAdd);
|
|
3939
4571
|
this.cdr.markForCheck();
|
|
3940
4572
|
}
|
|
4573
|
+
/** @deprecated Use {@link ToggleAddSelection}. */
|
|
4574
|
+
toggleAddSelection(testId) {
|
|
4575
|
+
return this.ToggleAddSelection(testId);
|
|
4576
|
+
}
|
|
4577
|
+
IsAddSelected(testId) {
|
|
4578
|
+
return this.SelectedTestIdsToAdd.has(testId);
|
|
4579
|
+
}
|
|
4580
|
+
/** @deprecated Use {@link IsAddSelected}. */
|
|
3941
4581
|
isAddSelected(testId) {
|
|
3942
|
-
return this.
|
|
4582
|
+
return this.IsAddSelected(testId);
|
|
3943
4583
|
}
|
|
3944
4584
|
/** Tests that match the picker search filter. */
|
|
3945
|
-
get
|
|
3946
|
-
const q = this.
|
|
4585
|
+
get FilteredAvailableTests() {
|
|
4586
|
+
const q = this.AddTestsSearch.trim().toLowerCase();
|
|
3947
4587
|
if (!q)
|
|
3948
|
-
return this.
|
|
3949
|
-
return this.
|
|
4588
|
+
return this.AvailableTests;
|
|
4589
|
+
return this.AvailableTests.filter(t => (t.Name || '').toLowerCase().includes(q) ||
|
|
3950
4590
|
(t.Description || '').toLowerCase().includes(q) ||
|
|
3951
4591
|
(t.Type || '').toLowerCase().includes(q));
|
|
3952
4592
|
}
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4593
|
+
/** @deprecated Use {@link FilteredAvailableTests}. */
|
|
4594
|
+
get filteredAvailableTests() {
|
|
4595
|
+
return this.FilteredAvailableTests;
|
|
4596
|
+
}
|
|
4597
|
+
SelectAllFiltered() {
|
|
4598
|
+
for (const t of this.FilteredAvailableTests) {
|
|
4599
|
+
this.SelectedTestIdsToAdd.add(t.ID);
|
|
3956
4600
|
}
|
|
3957
|
-
this.
|
|
4601
|
+
this.SelectedTestIdsToAdd = new Set(this.SelectedTestIdsToAdd);
|
|
3958
4602
|
this.cdr.markForCheck();
|
|
3959
4603
|
}
|
|
3960
|
-
|
|
3961
|
-
|
|
4604
|
+
/** @deprecated Use {@link SelectAllFiltered}. */
|
|
4605
|
+
selectAllFiltered() {
|
|
4606
|
+
return this.SelectAllFiltered();
|
|
4607
|
+
}
|
|
4608
|
+
ClearAddSelection() {
|
|
4609
|
+
this.SelectedTestIdsToAdd = new Set();
|
|
3962
4610
|
this.cdr.markForCheck();
|
|
3963
4611
|
}
|
|
4612
|
+
/** @deprecated Use {@link ClearAddSelection}. */
|
|
4613
|
+
clearAddSelection() {
|
|
4614
|
+
return this.ClearAddSelection();
|
|
4615
|
+
}
|
|
3964
4616
|
/** Create suite-test rows for every selected test, appended after existing ones. */
|
|
3965
|
-
async
|
|
3966
|
-
if (this.
|
|
4617
|
+
async ConfirmAddTests() {
|
|
4618
|
+
if (this.SelectedTestIdsToAdd.size === 0 || this.IsAddingTests)
|
|
3967
4619
|
return;
|
|
3968
|
-
this.
|
|
4620
|
+
this.IsAddingTests = true;
|
|
3969
4621
|
this.cdr.markForCheck();
|
|
3970
4622
|
const provider = this.ProviderToUse;
|
|
3971
|
-
const startSequence = this.
|
|
3972
|
-
const idsToAdd = Array.from(this.
|
|
4623
|
+
const startSequence = this.SuiteTests.reduce((max, st) => Math.max(max, st.Sequence ?? 0), 0);
|
|
4624
|
+
const idsToAdd = Array.from(this.SelectedTestIdsToAdd);
|
|
3973
4625
|
// Snapshot test names from the picker so we can hydrate the joined "Test"
|
|
3974
4626
|
// column on the optimistic rows (BaseEntity's joined view fields aren't
|
|
3975
4627
|
// always populated through a TransactionGroup save).
|
|
3976
|
-
const testNameById = new Map(this.
|
|
4628
|
+
const testNameById = new Map(this.AvailableTests.map(t => [t.ID, t.Name]));
|
|
3977
4629
|
try {
|
|
3978
4630
|
const tg = await provider.CreateTransactionGroup();
|
|
3979
4631
|
const newRows = [];
|
|
@@ -4002,12 +4654,12 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
4002
4654
|
}
|
|
4003
4655
|
}
|
|
4004
4656
|
}
|
|
4005
|
-
this.
|
|
4657
|
+
this.SuiteTests = [...this.SuiteTests, ...newRows];
|
|
4006
4658
|
this.cdr.markForCheck();
|
|
4007
4659
|
SharedService.Instance.CreateSimpleNotification(idsToAdd.length === 1 ? 'Test added to suite' : `${idsToAdd.length} tests added to suite`, 'success', 2000);
|
|
4008
|
-
this.
|
|
4009
|
-
this.
|
|
4010
|
-
this.
|
|
4660
|
+
this.ShowAddTestsDialog = false;
|
|
4661
|
+
this.SelectedTestIdsToAdd = new Set();
|
|
4662
|
+
this.AddTestsSearch = '';
|
|
4011
4663
|
}
|
|
4012
4664
|
else {
|
|
4013
4665
|
const failed = newRows.find(r => r.LatestResult && !r.LatestResult.Success);
|
|
@@ -4020,35 +4672,47 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
4020
4672
|
SharedService.Instance.CreateSimpleNotification(msg, 'error', 4000);
|
|
4021
4673
|
}
|
|
4022
4674
|
finally {
|
|
4023
|
-
this.
|
|
4675
|
+
this.IsAddingTests = false;
|
|
4024
4676
|
this.cdr.markForCheck();
|
|
4025
4677
|
}
|
|
4026
4678
|
}
|
|
4679
|
+
/** @deprecated Use {@link ConfirmAddTests}. */
|
|
4680
|
+
async confirmAddTests() {
|
|
4681
|
+
return this.ConfirmAddTests();
|
|
4682
|
+
}
|
|
4027
4683
|
/** Show the inline remove-confirm for one suite-test row. */
|
|
4028
|
-
|
|
4684
|
+
RequestRemoveTest(suiteTest, event) {
|
|
4029
4685
|
if (event)
|
|
4030
4686
|
event.stopPropagation();
|
|
4031
|
-
this.
|
|
4687
|
+
this.ConfirmingRemoveSuiteTestId = suiteTest.ID;
|
|
4032
4688
|
this.cdr.markForCheck();
|
|
4033
4689
|
}
|
|
4034
|
-
|
|
4690
|
+
/** @deprecated Use {@link RequestRemoveTest}. */
|
|
4691
|
+
requestRemoveTest(suiteTest, event) {
|
|
4692
|
+
return this.RequestRemoveTest(suiteTest, event);
|
|
4693
|
+
}
|
|
4694
|
+
CancelRemoveTest(event) {
|
|
4035
4695
|
if (event)
|
|
4036
4696
|
event.stopPropagation();
|
|
4037
|
-
this.
|
|
4697
|
+
this.ConfirmingRemoveSuiteTestId = null;
|
|
4038
4698
|
this.cdr.markForCheck();
|
|
4039
4699
|
}
|
|
4700
|
+
/** @deprecated Use {@link CancelRemoveTest}. */
|
|
4701
|
+
cancelRemoveTest(event) {
|
|
4702
|
+
return this.CancelRemoveTest(event);
|
|
4703
|
+
}
|
|
4040
4704
|
/** Delete the suite-test join row and refresh the list. */
|
|
4041
|
-
async
|
|
4705
|
+
async ConfirmRemoveTest(suiteTest, event) {
|
|
4042
4706
|
if (event)
|
|
4043
4707
|
event.stopPropagation();
|
|
4044
|
-
if (this.
|
|
4708
|
+
if (this.IsRemovingTest)
|
|
4045
4709
|
return;
|
|
4046
|
-
this.
|
|
4710
|
+
this.IsRemovingTest = true;
|
|
4047
4711
|
this.cdr.markForCheck();
|
|
4048
4712
|
try {
|
|
4049
4713
|
const ok = await suiteTest.Delete();
|
|
4050
4714
|
if (ok) {
|
|
4051
|
-
this.
|
|
4715
|
+
this.SuiteTests = this.SuiteTests.filter(t => !UUIDsEqual(t.ID, suiteTest.ID));
|
|
4052
4716
|
SharedService.Instance.CreateSimpleNotification('Test removed from suite', 'success', 2000);
|
|
4053
4717
|
}
|
|
4054
4718
|
else {
|
|
@@ -4061,19 +4725,23 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
4061
4725
|
SharedService.Instance.CreateSimpleNotification(msg, 'error', 4000);
|
|
4062
4726
|
}
|
|
4063
4727
|
finally {
|
|
4064
|
-
this.
|
|
4065
|
-
this.
|
|
4728
|
+
this.IsRemovingTest = false;
|
|
4729
|
+
this.ConfirmingRemoveSuiteTestId = null;
|
|
4066
4730
|
this.cdr.markForCheck();
|
|
4067
4731
|
}
|
|
4068
4732
|
}
|
|
4733
|
+
/** @deprecated Use {@link ConfirmRemoveTest}. */
|
|
4734
|
+
async confirmRemoveTest(suiteTest, event) {
|
|
4735
|
+
return this.ConfirmRemoveTest(suiteTest, event);
|
|
4736
|
+
}
|
|
4069
4737
|
/** Handle CDK drag-drop reordering — persists new Sequence values. */
|
|
4070
|
-
async
|
|
4738
|
+
async OnTestDrop(event) {
|
|
4071
4739
|
if (event.previousIndex === event.currentIndex)
|
|
4072
4740
|
return;
|
|
4073
4741
|
// Optimistic reorder
|
|
4074
|
-
const reordered = [...this.
|
|
4742
|
+
const reordered = [...this.SuiteTests];
|
|
4075
4743
|
moveItemInArray(reordered, event.previousIndex, event.currentIndex);
|
|
4076
|
-
this.
|
|
4744
|
+
this.SuiteTests = reordered;
|
|
4077
4745
|
this.cdr.markForCheck();
|
|
4078
4746
|
// Compute which rows actually changed sequence (1-based contiguous numbering)
|
|
4079
4747
|
const dirty = [];
|
|
@@ -4086,7 +4754,7 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
4086
4754
|
});
|
|
4087
4755
|
if (dirty.length === 0)
|
|
4088
4756
|
return;
|
|
4089
|
-
this.
|
|
4757
|
+
this.IsReorderingTests = true;
|
|
4090
4758
|
this.cdr.markForCheck();
|
|
4091
4759
|
const provider = this.ProviderToUse;
|
|
4092
4760
|
try {
|
|
@@ -4100,27 +4768,31 @@ let MJTestSuiteFormComponentExtended = class MJTestSuiteFormComponentExtended ex
|
|
|
4100
4768
|
const failed = dirty.find(r => r.LatestResult && !r.LatestResult.Success);
|
|
4101
4769
|
const detail = failed?.LatestResult?.CompleteMessage || 'Failed to save new order';
|
|
4102
4770
|
SharedService.Instance.CreateSimpleNotification(detail, 'error', 4000);
|
|
4103
|
-
this.
|
|
4771
|
+
this.TestsLoaded = false;
|
|
4104
4772
|
await this.loadTests();
|
|
4105
4773
|
}
|
|
4106
4774
|
}
|
|
4107
4775
|
catch (err) {
|
|
4108
4776
|
const msg = err instanceof Error ? err.message : 'Failed to save new order';
|
|
4109
4777
|
SharedService.Instance.CreateSimpleNotification(msg, 'error', 4000);
|
|
4110
|
-
this.
|
|
4778
|
+
this.TestsLoaded = false;
|
|
4111
4779
|
await this.loadTests();
|
|
4112
4780
|
}
|
|
4113
4781
|
finally {
|
|
4114
|
-
this.
|
|
4782
|
+
this.IsReorderingTests = false;
|
|
4115
4783
|
this.cdr.markForCheck();
|
|
4116
4784
|
}
|
|
4117
4785
|
}
|
|
4786
|
+
/** @deprecated Use {@link OnTestDrop}. */
|
|
4787
|
+
async onTestDrop(event) {
|
|
4788
|
+
return this.OnTestDrop(event);
|
|
4789
|
+
}
|
|
4118
4790
|
static { this.ɵfac = /*@__PURE__*/ (() => { let ɵMJTestSuiteFormComponentExtended_BaseFactory; return function MJTestSuiteFormComponentExtended_Factory(__ngFactoryType__) { return (ɵMJTestSuiteFormComponentExtended_BaseFactory || (ɵMJTestSuiteFormComponentExtended_BaseFactory = i0.ɵɵgetInheritedFactory(MJTestSuiteFormComponentExtended)))(__ngFactoryType__ || MJTestSuiteFormComponentExtended); }; })(); }
|
|
4119
4791
|
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MJTestSuiteFormComponentExtended, selectors: [["mj-test-suite-form"]], viewQuery: function MJTestSuiteFormComponentExtended_Query(rf, ctx) { if (rf & 1) {
|
|
4120
4792
|
i0.ɵɵviewQuery(_c0, 5);
|
|
4121
4793
|
} if (rf & 2) {
|
|
4122
4794
|
let _t;
|
|
4123
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
4795
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.ChartContainer = _t.first);
|
|
4124
4796
|
} }, hostBindings: function MJTestSuiteFormComponentExtended_HostBindings(rf, ctx) { if (rf & 1) {
|
|
4125
4797
|
i0.ɵɵlistener("keydown", function MJTestSuiteFormComponentExtended_keydown_HostBindingHandler($event) { return ctx.handleKeyboardShortcut($event); }, i0.ɵɵresolveDocument)("beforeunload", function MJTestSuiteFormComponentExtended_beforeunload_HostBindingHandler($event) { return ctx.handleBeforeUnload($event); }, i0.ɵɵresolveWindow);
|
|
4126
4798
|
} }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 87, vars: 51, consts: [["tagInput", ""], ["chartContainer", ""], ["pickerSearch", ""], [1, "test-suite-form"], [1, "suite-header"], ["aria-label", "Breadcrumb", 1, "breadcrumb"], ["href", "javascript:void(0)", 3, "click"], [1, "fas", "fa-vial"], [1, "breadcrumb-text"], [1, "current"], [1, "fas", "fa-chevron-right", "separator"], [1, "fas", "fa-layer-group"], [1, "header-content"], [1, "header-left"], [1, "suite-icon"], [1, "suite-info"], ["title", "You have unsaved changes", "aria-label", "Unsaved changes", 1, "dirty-indicator"], [1, "suite-meta"], [1, "status-badge", 3, "ngClass"], [1, "fas", 3, "ngClass"], [1, "test-count"], ["title", "You have unsaved changes", 1, "unsaved-pill"], [1, "header-actions"], ["mjButton", "", "title", "Export to CSV", 3, "click"], [1, "fas", "fa-file-excel"], ["mjButton", "", "variant", "primary", 3, "click"], [1, "fas", "fa-play"], ["mjButton", "", 3, "click", "disabled"], [1, "suite-description"], [1, "tabs-container"], ["role", "tablist", 1, "tabs"], ["role", "tab", 1, "tab", 3, "click"], [1, "fas", "fa-th-large"], [1, "fas", "fa-flask"], [1, "tab-badge"], [1, "fas", "fa-history"], [1, "fas", "fa-chart-line"], [1, "fas", "fa-balance-scale"], [1, "tab-content"], [1, "overview-tab"], [1, "tests-tab"], [1, "runs-tab"], [1, "analytics-tab"], [1, "compare-tab"], [1, "shortcuts-toggle", 3, "click", "title"], [1, "fas", "fa-keyboard"], [1, "keyboard-shortcuts"], ["Mode", "slide", 3, "Title", "Resizable"], ["role", "region", "aria-label", "Unsaved changes", 1, "save-bar"], [1, "save-bar-message"], ["aria-hidden", "true", 1, "save-bar-dot"], [1, "save-bar-text"], [1, "save-bar-fields"], [1, "save-bar-actions"], ["type", "button", "title", "Save changes (\u2318S)", 1, "save-bar-btn", "save-bar-btn--primary", 3, "click", "disabled"], ["type", "button", "title", "Discard changes", 1, "save-bar-btn", "save-bar-btn--ghost", 3, "click", "disabled"], [1, "fas", "fa-undo"], [1, "fas", "fa-circle"], [1, "edit-section"], [1, "edit-section-header"], [1, "fas", "fa-pen-to-square"], [1, "edit-section-sub"], [1, "edit-grid"], [1, "edit-field", "edit-field--full"], ["for", "suite-name"], [1, "required"], ["id", "suite-name", "type", "text", "placeholder", "e.g., Core Agent Test Suite", 1, "config-input", 3, "ngModelChange", "ngModel"], ["for", "suite-description"], ["id", "suite-description", "rows", "3", "placeholder", "What does this suite cover?", 1, "config-input", "config-textarea", 3, "ngModelChange", "ngModel"], [1, "edit-field"], ["for", "suite-status"], [1, "select-wrapper"], ["id", "suite-status", 1, "config-input", 3, "ngModelChange", "ngModel"], [3, "ngValue"], ["for", "suite-parent"], ["id", "suite-parent", 1, "config-input", 3, "ngModelChange", "ngModel"], ["for", "suite-tags"], [1, "tag-editor", 3, "click"], [1, "tag-chip-editable"], ["id", "suite-tags", "type", "text", 1, "tag-input-inline", 3, "ngModelChange", "keydown", "blur", "placeholder", "ngModel"], [1, "config-hint"], [1, "fas", "fa-cogs"], ["for", "suite-timeout"], ["id", "suite-timeout", "type", "number", "placeholder", "Default: 300000 (5 min)", 1, "config-input", 3, "ngModelChange", "ngModel"], [1, "meta-section"], [1, "meta-grid"], [1, "meta-item"], [1, "meta-label"], [1, "meta-value"], [1, "meta-value", "meta-mono"], ["type", "button", 1, "tag-remove", 3, "click"], [1, "fas", "fa-times"], [1, "tests-actionbar"], [1, "tests-actionbar-text"], [1, "tests-actionbar-count"], [1, "tests-actionbar-hint"], [1, "tests-actionbar-actions"], ["mjButton", "", "variant", "primary", 3, "click", "disabled"], [1, "fas", "fa-plus"], [1, "loading-state"], ["cdkDropList", "", 1, "tests-list"], ["Icon", "fa-solid fa-flask", "Title", "No Tests in Suite", "Message", "Add tests to this suite to start running them together.", "ActionText", "Add tests", "ActionIcon", "fa-solid fa-plus"], [1, "skeleton-list"], [1, "skeleton-card"], [1, "skeleton-sequence"], [1, "skeleton-icon"], [1, "skeleton-content"], [1, "skeleton-line", "wide"], [1, "skeleton-line", "narrow"], ["cdkDropList", "", 1, "tests-list", 3, "cdkDropListDropped"], ["cdkDrag", "", 1, "test-item", 3, "cdkDragDisabled"], ["cdkDragHandle", "", "title", "Drag to reorder", "aria-label", "Drag to reorder", 1, "test-drag-handle"], [1, "fas", "fa-grip-vertical"], [1, "test-sequence"], [1, "test-icon"], [1, "test-content", 3, "click"], [1, "test-name"], [1, "test-status"], [1, "test-remove-confirm"], [1, "fas", "fa-info-circle"], [1, "test-remove-confirm", 3, "click"], [1, "test-remove-confirm-text"], ["type", "button", 1, "test-remove-confirm-btn", "test-remove-confirm-btn--danger", 3, "click", "disabled"], [1, "fas", "fa-spinner", "fa-spin"], ["type", "button", 1, "test-remove-confirm-btn", "test-remove-confirm-btn--ghost", 3, "click", "disabled"], ["type", "button", "title", "Remove from suite", "aria-label", "Remove from suite", 1, "test-remove-btn", 3, "click"], [1, "fas", "fa-chevron-right", "test-chevron", 3, "click"], ["Icon", "fa-solid fa-flask", "Title", "No Tests in Suite", "Message", "Add tests to this suite to start running them together.", "ActionText", "Add tests", "ActionIcon", "fa-solid fa-plus", 3, "Action"], [1, "runs-list"], ["Icon", "fa-solid fa-circle-play", "Title", "No Suite Runs Yet", "Message", "Run this suite to see execution history and results here.", "ActionText", "Run Suite Now", "ActionIcon", "fa-solid fa-play"], [1, "run-item"], [1, "run-item", 3, "click"], [1, "run-icon"], [1, "fas"], [1, "run-content"], [1, "run-header"], [1, "run-id"], [1, "run-status"], [1, "run-meta"], [1, "fas", "fa-calendar"], [1, "run-eval-metrics"], [1, "eval-metric", "status", 3, "class"], ["title", "Human evaluation", 1, "eval-metric", "human"], ["title", "Auto score (pass rate)", 1, "eval-metric", "auto", 3, "high", "medium", "low"], [1, "run-tags"], [1, "fas", "fa-chevron-right"], [1, "fas", "fa-check-circle"], [1, "eval-metric", "status"], [1, "fas", "fa-user"], [1, "eval-pending"], [1, "fas", "fa-clock"], ["title", "Auto score (pass rate)", 1, "eval-metric", "auto"], [1, "fas", "fa-robot"], [1, "tag-chip"], ["Icon", "fa-solid fa-circle-play", "Title", "No Suite Runs Yet", "Message", "Run this suite to see execution history and results here.", "ActionText", "Run Suite Now", "ActionIcon", "fa-solid fa-play", 3, "Action"], ["Icon", "fa-solid fa-chart-line", "Title", "No Analytics Data", "Message", "Run this suite to start collecting analytics data.", "ActionText", "Run Suite Now", "ActionIcon", "fa-solid fa-play"], ["text", "Loading analytics data..."], [1, "analytics-subnav"], [1, "subnav-tabs"], [1, "subnav-tab", 3, "click"], [1, "fas", "fa-chart-bar"], [1, "fas", "fa-th"], [1, "fas", "fa-project-diagram"], ["Size", "sm", 3, "ExpandedChange", "FlushBody", "Expanded"], ["mjAccordionTitle", ""], ["mjAccordionBody", ""], [1, "fas", "fa-filter", 2, "color", "var(--test-primary)"], [1, "mj-accordion-badge", "mj-accordion-badge--right", "mj-accordion-badge--muted"], [1, "filters-content"], [1, "filter-group"], [1, "filter-buttons"], [1, "filter-btn", 3, "click"], [1, "filter-hint"], [1, "filter-buttons", "tag-filters"], [1, "filter-btn", "tag-btn", "all-tags-btn", 3, "click"], [1, "filter-btn", "tag-btn", 3, "active"], [1, "filter-btn", "tag-btn", 3, "click"], [1, "fas", "fa-check"], [1, "analytics-kpis"], [1, "kpi-card"], [1, "kpi-icon"], [1, "fas", "fa-play-circle"], [1, "kpi-content"], [1, "kpi-value"], [1, "kpi-label"], [1, "kpi-icon", "success"], [1, "kpi-trend", 3, "ngClass"], [1, "kpi-icon", "info"], [1, "kpi-icon", "warning"], [1, "fas", "fa-dollar-sign"], [1, "analytics-table-section"], [1, "fas", "fa-table"], [1, "analytics-table-wrapper"], [1, "analytics-table"], [1, "clickable-row"], ["Variant", "no-results", "Size", "compact", "Title", "No runs match the current filters."], [1, "clickable-row", 3, "click"], [1, "status-chip", 3, "ngClass"], [1, "pass-rate-cell"], [1, "pass-rate-bar", 3, "ngClass"], [1, "tag-cell"], [1, "tag-chip-table"], [1, "tag-more"], [1, "matrix-section"], ["Variant", "no-results", "Icon", "fa-solid fa-table-cells", "Title", "No Matrix Data", "Message", "No suite runs match the current filters."], ["text", "Loading test matrix..."], [1, "matrix-header"], [1, "matrix-header-right"], [1, "matrix-filter-input"], [1, "fas", "fa-search"], ["type", "text", "placeholder", "Filter tests...", 1, "filter-input", 3, "input", "value"], ["title", "Clear filter", 1, "clear-filter-btn"], [1, "matrix-run-count"], ["mjButton", "", "title", "Export matrix to CSV", 3, "click", "disabled"], [1, "fas", "fa-download"], [1, "matrix-scroll-container"], [1, "test-matrix"], ["title", "Sort by sequence", 1, "seq-header", 3, "click"], ["title", "Sort by name", 1, "test-name-header", 3, "click"], [1, "run-header", 3, "title"], [1, "spacer-header"], [3, "row-selected"], [1, "totals-row"], [1, "seq-cell", "totals-label"], [1, "test-name-cell", "totals-label"], [1, "result-cell", "totals-cell"], [1, "spacer-cell"], ["title", "Clear filter", 1, "clear-filter-btn", 3, "click"], [1, "run-header", 3, "click", "title"], [1, "run-header-content"], [1, "run-tags-header"], [1, "run-date"], [1, "run-pass-rate", 3, "ngClass"], [1, "tag-chip-header"], [1, "tag-more-header"], [3, "click"], [1, "seq-cell"], [1, "test-name-cell"], [1, "test-name", 3, "title"], [1, "result-cell", 3, "ngClass", "clickable", "cell-not-run", "title"], [1, "result-cell", 3, "click", "ngClass", "title"], [1, "cell-eval-stack"], [1, "cell-not-run-indicator"], [1, "cell-status", 3, "ngClass", "cell-skipped-status", "title"], ["title", "Human Review: No rating submitted yet", 1, "cell-human", "no-feedback"], [1, "cell-human", "has-feedback", 3, "rating-low", "rating-medium", "rating-good", "rating-excellent", "title"], [1, "cell-auto", "has-score", 3, "score-low", "score-medium", "score-good", "score-excellent", "title"], ["title", "Auto Score: No automated score available", 1, "cell-auto", "no-score"], [1, "cell-status", 3, "ngClass", "title"], [1, "fas", "fa-user-slash"], [1, "cell-human", "has-feedback", 3, "title"], [1, "rating-value"], [1, "cell-auto", "has-score", 3, "title"], [1, "score-value"], [1, "fas", "fa-minus"], [1, "cell-eval-stack", "totals-stack"], [1, "totals-status"], [1, "totals-human"], [1, "totals-auto"], [1, "pass-count"], [1, "avg-label"], [1, "count-label"], [1, "chart-section"], ["Variant", "no-results", "Icon", "fa-solid fa-diagram-project", "Title", "No Chart Data", "Message", "No suite runs match the current filters."], ["text", "Loading chart data..."], [1, "chart-header"], [1, "chart-legend"], [1, "legend-item", "chart-passed"], [1, "legend-item", "chart-failed"], [1, "legend-item", "chart-error"], [1, "fas", "fa-exclamation"], [1, "legend-item", "chart-skipped"], [1, "fas", "fa-forward"], [1, "chart-container"], [1, "d3-chart"], [1, "chart-info"], ["Icon", "fa-solid fa-chart-line", "Title", "No Analytics Data", "Message", "Run this suite to start collecting analytics data.", "ActionText", "Run Suite Now", "ActionIcon", "fa-solid fa-play", 3, "Action"], [1, "compare-selection"], [1, "compare-run-selector"], [1, "run-selector-list"], [1, "selected-run-preview"], [1, "compare-vs"], [1, "fas", "fa-exchange-alt"], [1, "compare-results"], ["Icon", "fa-solid fa-scale-balanced", "Title", "Select Two Runs to Compare", "Message", "Choose a baseline run (A) and a comparison run (B) from the lists above to see a detailed side-by-side comparison."], ["Icon", "fa-solid fa-scale-balanced", "Title", "Not Enough Runs to Compare", "Message", "You need at least 2 suite runs to use the comparison feature.", "ActionText", "Run Suite Now", "ActionIcon", "fa-solid fa-play"], [1, "run-selector-item", 3, "selected"], [1, "run-selector-item", 3, "click"], [1, "selector-status"], [1, "selector-content"], [1, "selector-date"], [1, "selector-rate"], [1, "selector-tags"], [1, "tag-mini"], [1, "preview-header"], [1, "preview-label"], [1, "clear-btn", 3, "click"], [1, "preview-details"], [1, "preview-rate"], [1, "run-selector-item", 3, "selected", "disabled"], [1, "compare-summary"], [1, "compare-summary-card"], [1, "summary-label"], [1, "summary-value", 3, "ngClass"], [1, "compare-summary-card", "improved"], [1, "summary-value"], [1, "compare-summary-card", "regressed"], [1, "compare-table-section"], [1, "fas", "fa-list"], [1, "compare-table-wrapper"], [1, "compare-table"], [3, "ngClass"], [1, "status-chip", "status-missing"], [1, "muted"], [1, "change-indicator", "improved"], [1, "change-indicator", "regressed"], [1, "change-indicator", "unchanged"], [1, "fas", "fa-arrow-up"], [1, "fas", "fa-arrow-down"], ["text", "Loading comparison data..."], ["Icon", "fa-solid fa-scale-balanced", "Title", "Not Enough Runs to Compare", "Message", "You need at least 2 suite runs to use the comparison feature.", "ActionText", "Run Suite Now", "ActionIcon", "fa-solid fa-play", 3, "Action"], [1, "shortcuts-header"], ["title", "Hide shortcuts", 1, "shortcuts-close", 3, "click"], [1, "shortcut-list"], [1, "shortcut-item"], [1, "shortcut-keys"], ["Mode", "slide", 3, "Closed", "Title", "Resizable"], [3, "PanelClose", "PanelMode", "selectedTestId", "selectedSuiteId", "runMode"], [1, "picker-backdrop", 3, "click"], ["role", "dialog", "aria-modal", "true", "aria-labelledby", "add-tests-dialog-title", 1, "picker-dialog", 3, "click"], [1, "picker-header"], ["id", "add-tests-dialog-title", 1, "picker-title"], [1, "fas", "fa-plus-circle"], ["type", "button", "aria-label", "Close", 1, "picker-close", 3, "click", "disabled"], [1, "picker-search"], [1, "fas", "fa-search", "picker-search-icon"], ["type", "text", "placeholder", "Search tests by name, description, or type...", "autofocus", "", 1, "picker-search-input", 3, "ngModelChange", "ngModel"], ["type", "button", "aria-label", "Clear search", 1, "picker-search-clear"], [1, "picker-toolbar"], [1, "picker-toolbar-status"], [1, "picker-toolbar-actions"], ["type", "button", 1, "picker-link-btn", 3, "click", "disabled"], [1, "picker-body"], [1, "picker-loading"], [1, "picker-list"], [1, "picker-footer"], ["type", "button", 1, "picker-btn", "picker-btn--primary", 3, "click", "disabled"], ["type", "button", 1, "picker-btn", "picker-btn--ghost", 3, "click", "disabled"], ["type", "button", "aria-label", "Clear search", 1, "picker-search-clear", 3, "click"], ["text", "Loading tests..."], ["Variant", "success", "Size", "compact", "Title", "Everything's already in the suite", "Message", "This suite contains every available test. Create a new test first to add more."], ["Variant", "no-results", "Size", "compact", "Title", "No matches", 3, "Message"], ["role", "checkbox", "tabindex", "0", 1, "picker-row", 3, "picker-row--selected"], ["role", "checkbox", "tabindex", "0", 1, "picker-row", 3, "click"], [1, "picker-check"], [1, "picker-row-main"], [1, "picker-row-name"], [1, "picker-row-desc"], [1, "picker-row-meta"], [1, "picker-row-pill"], [1, "picker-row-pill", "picker-row-pill--status"], [1, "save-bar-kbd"]], template: function MJTestSuiteFormComponentExtended_Template(rf, ctx) { if (rf & 1) {
|
|
@@ -4315,7 +4987,7 @@ export { MJTestSuiteFormComponentExtended };
|
|
|
4315
4987
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(MJTestSuiteFormComponentExtended, [{
|
|
4316
4988
|
type: Component,
|
|
4317
4989
|
args: [{ standalone: false, selector: 'mj-test-suite-form', changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"test-suite-form\">\n <!-- Header Section -->\n <div class=\"suite-header\">\n <!-- Breadcrumb Navigation -->\n <nav class=\"breadcrumb\" aria-label=\"Breadcrumb\">\n <ol>\n <li>\n <a href=\"javascript:void(0)\" (click)=\"navigateToTestingDashboard()\">\n <i class=\"fas fa-vial\"></i>\n <span class=\"breadcrumb-text\">Testing</span>\n </a>\n </li>\n <li class=\"current\">\n <i class=\"fas fa-chevron-right separator\"></i>\n <i class=\"fas fa-layer-group\"></i>\n <span>{{ record.Name }}</span>\n </li>\n </ol>\n </nav>\n\n <div class=\"header-content\">\n <div class=\"header-left\">\n <div class=\"suite-icon\" [style.background-color]=\"getStatusColor()\">\n <i class=\"fas fa-layer-group\"></i>\n </div>\n <div class=\"suite-info\">\n <h1>\n {{ record.Name }}\n @if (isDirty) {\n <span class=\"dirty-indicator\" title=\"You have unsaved changes\" aria-label=\"Unsaved changes\">\u25CF</span>\n }\n </h1>\n <div class=\"suite-meta\">\n <span class=\"status-badge\" [ngClass]=\"getStatusClass()\">\n <i class=\"fas\" [ngClass]=\"record.Status === 'Active' ? 'fa-circle-check' : 'fa-circle-pause'\"></i>\n {{ record.Status }}\n </span>\n @if (testsLoaded) {\n <span class=\"test-count\">\n <i class=\"fas fa-flask\"></i>\n {{ suiteTests.length }} tests\n </span>\n }\n @if (isDirty) {\n <span class=\"unsaved-pill\" title=\"You have unsaved changes\">\n <i class=\"fas fa-circle\"></i> Unsaved\n </span>\n }\n </div>\n </div>\n </div>\n <div class=\"header-actions\">\n <app-evaluation-mode-toggle></app-evaluation-mode-toggle>\n <button mjButton (click)=\"exportToExcel()\" title=\"Export to CSV\">\n <i class=\"fas fa-file-excel\"></i> Export\n </button>\n <button mjButton (click)=\"runSuite()\" variant=\"primary\">\n <i class=\"fas fa-play\"></i> Run Suite\n </button>\n <button mjButton (click)=\"refresh()\" [disabled]=\"isRefreshing\">\n <i class=\"fas\" [ngClass]=\"isRefreshing ? 'fa-sync fa-spin' : 'fa-sync'\"></i>\n {{ isRefreshing ? 'Refreshing...' : 'Refresh' }}\n </button>\n </div>\n </div>\n @if (record.Description) {\n <div class=\"suite-description\">\n <p>{{ record.Description }}</p>\n </div>\n }\n </div>\n\n <!-- Tabs -->\n <div class=\"tabs-container\">\n <div class=\"tabs\" role=\"tablist\">\n <button class=\"tab\"\n [class.active]=\"activeTab === 'overview'\"\n (click)=\"changeTab('overview')\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'overview'\">\n <i class=\"fas fa-th-large\"></i> Overview\n </button>\n <button class=\"tab\"\n [class.active]=\"activeTab === 'tests'\"\n (click)=\"changeTab('tests')\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'tests'\">\n <i class=\"fas fa-flask\"></i> Tests\n @if (testsLoaded) {\n <span class=\"tab-badge\">{{ suiteTests.length }}</span>\n }\n </button>\n <button class=\"tab\"\n [class.active]=\"activeTab === 'runs'\"\n (click)=\"changeTab('runs')\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'runs'\">\n <i class=\"fas fa-history\"></i> Runs\n @if (runsLoaded) {\n <span class=\"tab-badge\">{{ suiteRuns.length }}</span>\n }\n </button>\n <button class=\"tab\"\n [class.active]=\"activeTab === 'analytics'\"\n (click)=\"changeTab('analytics')\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'analytics'\">\n <i class=\"fas fa-chart-line\"></i> Analytics\n </button>\n <button class=\"tab\"\n [class.active]=\"activeTab === 'compare'\"\n (click)=\"changeTab('compare')\"\n role=\"tab\"\n [attr.aria-selected]=\"activeTab === 'compare'\">\n <i class=\"fas fa-balance-scale\"></i> Compare\n </button>\n </div>\n </div>\n\n <!-- Tab Content -->\n <div class=\"tab-content\" [class.has-savebar]=\"isDirty\">\n <!-- Overview Tab -->\n @if (activeTab === 'overview') {\n <div class=\"overview-tab\">\n <!-- Editable Details -->\n <section class=\"edit-section\">\n <header class=\"edit-section-header\">\n <h3><i class=\"fas fa-pen-to-square\"></i> Details</h3>\n <p class=\"edit-section-sub\">Edit any field \u2014 changes are saved with the bar at the bottom.</p>\n </header>\n <div class=\"edit-grid\">\n <div class=\"edit-field edit-field--full\">\n <label for=\"suite-name\">Name <span class=\"required\">*</span></label>\n <input\n id=\"suite-name\"\n type=\"text\"\n class=\"config-input\"\n placeholder=\"e.g., Core Agent Test Suite\"\n [(ngModel)]=\"record.Name\"\n />\n </div>\n\n <div class=\"edit-field edit-field--full\">\n <label for=\"suite-description\">Description</label>\n <textarea\n id=\"suite-description\"\n class=\"config-input config-textarea\"\n rows=\"3\"\n placeholder=\"What does this suite cover?\"\n [ngModel]=\"record.Description\"\n (ngModelChange)=\"record.Description = $event || null\"\n ></textarea>\n </div>\n\n <div class=\"edit-field\">\n <label for=\"suite-status\">Status</label>\n <div class=\"select-wrapper\">\n <select\n id=\"suite-status\"\n class=\"config-input\"\n [(ngModel)]=\"record.Status\"\n >\n @for (opt of statusOptions; track opt) {\n <option [ngValue]=\"opt\">{{ opt }}</option>\n }\n </select>\n </div>\n </div>\n\n <div class=\"edit-field\">\n <label for=\"suite-parent\">Parent Suite</label>\n <select\n id=\"suite-parent\"\n class=\"config-input\"\n [ngModel]=\"record.ParentID\"\n (ngModelChange)=\"record.ParentID = $event || null\"\n >\n <option [ngValue]=\"null\">\u2014 None (top-level) \u2014</option>\n @for (s of parentSuiteOptions; track s.ID) {\n <option [ngValue]=\"s.ID\">{{ s.Name }}</option>\n }\n </select>\n </div>\n\n <div class=\"edit-field edit-field--full\">\n <label for=\"suite-tags\">Tags</label>\n <div class=\"tag-editor\" (click)=\"tagInput.focus()\">\n @for (tag of tags; track tag) {\n <span class=\"tag-chip-editable\">\n {{ tag }}\n <button type=\"button\" class=\"tag-remove\" (click)=\"removeTag(tag); $event.stopPropagation()\" [attr.aria-label]=\"'Remove tag ' + tag\">\n <i class=\"fas fa-times\"></i>\n </button>\n </span>\n }\n <input\n #tagInput\n id=\"suite-tags\"\n type=\"text\"\n class=\"tag-input-inline\"\n [placeholder]=\"tags.length === 0 ? 'Add tags, press Enter or comma...' : ''\"\n [(ngModel)]=\"tagDraft\"\n (keydown)=\"onTagInputKeydown($event)\"\n (blur)=\"addTagFromDraft()\"\n />\n </div>\n <span class=\"config-hint\">Press Enter or comma to add a tag. Backspace on empty input removes the last tag.</span>\n </div>\n </div>\n </section>\n\n <!-- Execution Settings -->\n <section class=\"edit-section\">\n <header class=\"edit-section-header\">\n <h3><i class=\"fas fa-cogs\"></i> Execution Settings</h3>\n </header>\n <div class=\"edit-grid\">\n <div class=\"edit-field\">\n <label for=\"suite-timeout\">Max Execution Time (ms)</label>\n <input\n id=\"suite-timeout\"\n type=\"number\"\n class=\"config-input\"\n placeholder=\"Default: 300000 (5 min)\"\n [ngModel]=\"record.MaxExecutionTimeMS\"\n (ngModelChange)=\"record.MaxExecutionTimeMS = $event === '' || $event === null ? null : +$event\"\n />\n <span class=\"config-hint\">Default timeout for tests in this suite. Currently: {{ formatTimeout(record.MaxExecutionTimeMS) }}</span>\n </div>\n </div>\n </section>\n\n <!-- Metadata (read-only) -->\n <section class=\"meta-section\">\n <div class=\"meta-grid\">\n <div class=\"meta-item\">\n <div class=\"meta-label\">Created</div>\n <div class=\"meta-value\">{{ record.__mj_CreatedAt | date:'medium' }}</div>\n </div>\n <div class=\"meta-item\">\n <div class=\"meta-label\">Last updated</div>\n <div class=\"meta-value\">{{ record.__mj_UpdatedAt | date:'medium' }}</div>\n </div>\n <div class=\"meta-item\">\n <div class=\"meta-label\">Suite ID</div>\n <div class=\"meta-value meta-mono\">{{ record.ID }}</div>\n </div>\n </div>\n </section>\n </div>\n }\n\n <!-- Tests Tab -->\n @if (activeTab === 'tests') {\n <div class=\"tests-tab\">\n <!-- Header: action bar -->\n <div class=\"tests-actionbar\">\n <div class=\"tests-actionbar-text\">\n <strong>Suite tests</strong>\n @if (testsLoaded) {\n <span class=\"tests-actionbar-count\">{{ suiteTests.length }}</span>\n }\n <span class=\"tests-actionbar-hint\">Drag to reorder \u00B7 Hover a row to remove</span>\n </div>\n <div class=\"tests-actionbar-actions\">\n <button mjButton variant=\"primary\" (click)=\"openAddTestsDialog()\" [disabled]=\"isReorderingTests\">\n <i class=\"fas fa-plus\"></i> Add tests\n </button>\n </div>\n </div>\n\n <!-- Loading State -->\n @if (loadingTests) {\n <div class=\"loading-state\">\n <div class=\"skeleton-list\">\n @for (i of [1,2,3,4,5]; track i) {\n <div class=\"skeleton-card\">\n <div class=\"skeleton-sequence\"></div>\n <div class=\"skeleton-icon\"></div>\n <div class=\"skeleton-content\">\n <div class=\"skeleton-line wide\"></div>\n <div class=\"skeleton-line narrow\"></div>\n </div>\n </div>\n }\n </div>\n </div>\n }\n <!-- Tests List -->\n @if (!loadingTests && suiteTests.length > 0) {\n <div class=\"tests-list\" cdkDropList (cdkDropListDropped)=\"onTestDrop($event)\">\n @for (test of suiteTests; track test.ID) {\n <div class=\"test-item\" cdkDrag [cdkDragDisabled]=\"isReorderingTests || isRemovingTest\">\n <span class=\"test-drag-handle\" cdkDragHandle title=\"Drag to reorder\" aria-label=\"Drag to reorder\">\n <i class=\"fas fa-grip-vertical\"></i>\n </span>\n <div class=\"test-sequence\">{{ test.Sequence }}</div>\n <div class=\"test-icon\"><i class=\"fas fa-flask\"></i></div>\n <div class=\"test-content\" (click)=\"openTest(test.TestID)\">\n <div class=\"test-name\">{{ test.Test }}</div>\n <div class=\"test-status\">\n @if (test.Status) {\n <span><i class=\"fas fa-info-circle\"></i> {{ test.Status }}</span>\n }\n </div>\n </div>\n <!-- Inline remove confirm OR hover-reveal X -->\n @if (confirmingRemoveSuiteTestId === test.ID) {\n <div class=\"test-remove-confirm\" (click)=\"$event.stopPropagation()\">\n <span class=\"test-remove-confirm-text\">Remove from suite?</span>\n <button\n type=\"button\"\n class=\"test-remove-confirm-btn test-remove-confirm-btn--danger\"\n (click)=\"confirmRemoveTest(test, $event)\"\n [disabled]=\"isRemovingTest\"\n >\n @if (isRemovingTest) {\n <i class=\"fas fa-spinner fa-spin\"></i>\n } @else {\n Remove\n }\n </button>\n <button\n type=\"button\"\n class=\"test-remove-confirm-btn test-remove-confirm-btn--ghost\"\n (click)=\"cancelRemoveTest($event)\"\n [disabled]=\"isRemovingTest\"\n >\n Cancel\n </button>\n </div>\n } @else {\n <button\n type=\"button\"\n class=\"test-remove-btn\"\n title=\"Remove from suite\"\n aria-label=\"Remove from suite\"\n (click)=\"requestRemoveTest(test, $event)\"\n >\n <i class=\"fas fa-times\"></i>\n </button>\n <i class=\"fas fa-chevron-right test-chevron\" (click)=\"openTest(test.TestID)\"></i>\n }\n </div>\n }\n </div>\n }\n <!-- Empty State -->\n @if (testsLoaded && !loadingTests && suiteTests.length === 0) {\n <mj-empty-state\n Icon=\"fa-solid fa-flask\"\n Title=\"No Tests in Suite\"\n Message=\"Add tests to this suite to start running them together.\"\n ActionText=\"Add tests\"\n ActionIcon=\"fa-solid fa-plus\"\n (Action)=\"openAddTestsDialog()\" />\n }\n </div>\n }\n\n <!-- Runs Tab -->\n @if (activeTab === 'runs') {\n <div class=\"runs-tab\">\n <!-- Loading State -->\n @if (loadingRuns) {\n <div class=\"loading-state\">\n <div class=\"skeleton-list\">\n @for (i of [1,2,3]; track i) {\n <div class=\"skeleton-card\">\n <div class=\"skeleton-icon\"></div>\n <div class=\"skeleton-content\">\n <div class=\"skeleton-line wide\"></div>\n <div class=\"skeleton-line narrow\"></div>\n </div>\n </div>\n }\n </div>\n </div>\n }\n <!-- Runs List -->\n @if (!loadingRuns && suiteRuns.length > 0) {\n <div class=\"runs-list\">\n @for (run of suiteRuns; track run) {\n <div class=\"run-item\" (click)=\"openSuiteRun(run.ID)\">\n <div class=\"run-icon\" [style.background-color]=\"getRunStatusColor(run.Status)\">\n <i class=\"fas\"\n [class.fa-check]=\"run.Status === 'Completed'\"\n [class.fa-times]=\"run.Status === 'Failed'\"\n [class.fa-spinner]=\"run.Status === 'Running'\"\n [class.fa-clock]=\"run.Status === 'Pending'\"\n [class.fa-ban]=\"run.Status === 'Cancelled'\"></i>\n </div>\n <div class=\"run-content\">\n <div class=\"run-header\">\n <span class=\"run-id\">Run #{{ run.ID.substring(0, 8) }}</span>\n <span class=\"run-status\" [style.color]=\"getRunStatusColor(run.Status)\">{{ run.Status }}</span>\n </div>\n <div class=\"run-meta\">\n <span><i class=\"fas fa-calendar\"></i> {{ getRelativeTime(run.StartedAt) }}</span>\n @if (run.TotalTests) {\n <span>\n <i class=\"fas fa-check-circle\"></i> {{ run.PassedTests }}/{{ run.TotalTests }}\n ({{ getPassRate(run).toFixed(0) }}%)\n </span>\n }\n </div>\n <!-- Evaluation metrics row -->\n <div class=\"run-eval-metrics\">\n <!-- Status badge -->\n @if (evalPreferences.showExecution) {\n <span class=\"eval-metric status\" [class]=\"'status-' + run.Status.toLowerCase()\">\n <i class=\"fas\"\n [class.fa-circle-check]=\"run.Status === 'Completed'\"\n [class.fa-circle-xmark]=\"run.Status === 'Failed'\"\n [class.fa-spinner]=\"run.Status === 'Running'\"\n [class.fa-clock]=\"run.Status === 'Pending'\"\n [class.fa-ban]=\"run.Status === 'Cancelled'\"></i>\n </span>\n }\n <!-- Human score (placeholder - need avg from suite run) -->\n @if (evalPreferences.showHuman) {\n <span class=\"eval-metric human\" title=\"Human evaluation\">\n <i class=\"fas fa-user\"></i>\n <span class=\"eval-pending\"><i class=\"fas fa-clock\"></i></span>\n </span>\n }\n <!-- Auto score (pass rate as proxy) -->\n @if (evalPreferences.showAuto && run.TotalTests) {\n <span class=\"eval-metric auto\"\n [class.high]=\"getPassRate(run) >= 80\"\n [class.medium]=\"getPassRate(run) >= 50 && getPassRate(run) < 80\"\n [class.low]=\"getPassRate(run) < 50\"\n title=\"Auto score (pass rate)\">\n <i class=\"fas fa-robot\"></i>\n <span>{{ getPassRate(run).toFixed(0) }}%</span>\n </span>\n }\n </div>\n <!-- Tags display -->\n @if (getRunTags(run).length > 0) {\n <div class=\"run-tags\">\n @for (tag of getRunTags(run); track tag) {\n <span class=\"tag-chip\">{{ tag }}</span>\n }\n </div>\n }\n </div>\n <i class=\"fas fa-chevron-right\"></i>\n </div>\n }\n </div>\n }\n <!-- Empty State -->\n @if (runsLoaded && !loadingRuns && suiteRuns.length === 0) {\n <mj-empty-state\n Icon=\"fa-solid fa-circle-play\"\n Title=\"No Suite Runs Yet\"\n Message=\"Run this suite to see execution history and results here.\"\n ActionText=\"Run Suite Now\"\n ActionIcon=\"fa-solid fa-play\"\n (Action)=\"runSuite()\" />\n }\n </div>\n }\n\n <!-- Analytics Tab -->\n @if (activeTab === 'analytics') {\n <div class=\"analytics-tab\">\n <!-- Loading State -->\n @if (loadingAnalytics) {\n <div class=\"loading-state\">\n <mj-loading text=\"Loading analytics data...\"></mj-loading>\n </div>\n }\n @if (!loadingAnalytics && analyticsLoaded) {\n <!-- View Toggle Sub-nav -->\n <div class=\"analytics-subnav\">\n <div class=\"subnav-tabs\">\n <button class=\"subnav-tab\"\n [class.active]=\"analyticsView === 'summary'\"\n (click)=\"setAnalyticsView('summary')\">\n <i class=\"fas fa-chart-bar\"></i>\n <span>Summary</span>\n </button>\n <button class=\"subnav-tab\"\n [class.active]=\"analyticsView === 'matrix'\"\n (click)=\"setAnalyticsView('matrix')\">\n <i class=\"fas fa-th\"></i>\n <span>Matrix</span>\n </button>\n <button class=\"subnav-tab\"\n [class.active]=\"analyticsView === 'chart'\"\n (click)=\"setAnalyticsView('chart')\">\n <i class=\"fas fa-project-diagram\"></i>\n <span>Chart</span>\n </button>\n </div>\n </div>\n <!-- Collapsible Filters (shared by both views) -->\n <mj-accordion-panel Size=\"sm\" [FlushBody]=\"true\"\n [Expanded]=\"!filtersCollapsed\" (ExpandedChange)=\"onFiltersExpandedChange($event)\">\n <ng-template mjAccordionTitle>\n <i class=\"fas fa-filter\" style=\"color: var(--test-primary);\"></i>\n Filters\n @if (filtersCollapsed) {\n <span class=\"mj-accordion-badge mj-accordion-badge--right mj-accordion-badge--muted\">\n {{ analyticsTimeRange === 'all' ? 'All Time' : analyticsTimeRange }}\n @if (selectedTags.length > 0) {\n <span> \u00B7 {{ selectedTags.length }} tags</span>\n }\n </span>\n }\n </ng-template>\n <ng-template mjAccordionBody>\n <div class=\"filters-content\">\n <div class=\"filter-group\">\n <label>Time Range</label>\n <div class=\"filter-buttons\">\n <button class=\"filter-btn\" [class.active]=\"analyticsTimeRange === '7d'\" (click)=\"setTimeRange('7d')\">7 Days</button>\n <button class=\"filter-btn\" [class.active]=\"analyticsTimeRange === '30d'\" (click)=\"setTimeRange('30d')\">30 Days</button>\n <button class=\"filter-btn\" [class.active]=\"analyticsTimeRange === '90d'\" (click)=\"setTimeRange('90d')\">90 Days</button>\n <button class=\"filter-btn\" [class.active]=\"analyticsTimeRange === 'all'\" (click)=\"setTimeRange('all')\">All Time</button>\n </div>\n </div>\n @if (uniqueTags.length > 0) {\n <div class=\"filter-group\">\n <label>Filter by Tag @if (selectedTags.length > 0) {\n <span class=\"filter-hint\">({{ selectedTags.length }} selected)</span>\n }</label>\n <div class=\"filter-buttons tag-filters\">\n <button class=\"filter-btn tag-btn all-tags-btn\"\n [class.active]=\"selectedTags.length === 0\"\n (click)=\"toggleTagFilter(null)\">\n <i class=\"fas fa-layer-group\"></i> All Tags\n </button>\n @for (tag of uniqueTags; track tag) {\n <button class=\"filter-btn tag-btn\"\n [class.active]=\"isTagSelected(tag)\"\n (click)=\"toggleTagFilter(tag)\">\n @if (isTagSelected(tag)) {\n <i class=\"fas fa-check\"></i>\n }\n {{ tag }}\n </button>\n }\n </div>\n </div>\n }\n </div>\n </ng-template>\n </mj-accordion-panel>\n <!-- Summary View -->\n @if (analyticsView === 'summary') {\n <!-- KPI Cards -->\n <div class=\"analytics-kpis\">\n <div class=\"kpi-card\">\n <div class=\"kpi-icon\"><i class=\"fas fa-play-circle\"></i></div>\n <div class=\"kpi-content\">\n <div class=\"kpi-value\">{{ getTotalRuns() }}</div>\n <div class=\"kpi-label\">Total Runs</div>\n </div>\n </div>\n <div class=\"kpi-card\">\n <div class=\"kpi-icon success\"><i class=\"fas fa-check-circle\"></i></div>\n <div class=\"kpi-content\">\n <div class=\"kpi-value\">{{ getAveragePassRate().toFixed(1) }}%</div>\n <div class=\"kpi-label\">Avg Pass Rate</div>\n <div class=\"kpi-trend\" [ngClass]=\"{'trend-up': getPassRateTrend().direction === 'up', 'trend-down': getPassRateTrend().direction === 'down'}\">\n <i class=\"fas\" [ngClass]=\"{'fa-arrow-up': getPassRateTrend().direction === 'up', 'fa-arrow-down': getPassRateTrend().direction === 'down', 'fa-minus': getPassRateTrend().direction === 'stable'}\"></i>\n {{ getPassRateTrend().value.toFixed(1) }}%\n </div>\n </div>\n </div>\n <div class=\"kpi-card\">\n <div class=\"kpi-icon info\"><i class=\"fas fa-clock\"></i></div>\n <div class=\"kpi-content\">\n <div class=\"kpi-value\">{{ formatDuration(getAverageDuration()) }}</div>\n <div class=\"kpi-label\">Avg Duration</div>\n </div>\n </div>\n <div class=\"kpi-card\">\n <div class=\"kpi-icon warning\"><i class=\"fas fa-dollar-sign\"></i></div>\n <div class=\"kpi-content\">\n <div class=\"kpi-value\">{{ formatCost(getTotalCost()) }}</div>\n <div class=\"kpi-label\">Total Cost</div>\n </div>\n </div>\n </div>\n <!-- Runs Table -->\n <div class=\"analytics-table-section\">\n <h3><i class=\"fas fa-table\"></i> Run History</h3>\n <div class=\"analytics-table-wrapper\">\n <table class=\"analytics-table\">\n <thead>\n <tr>\n <th>Date</th>\n <th>Status</th>\n <th>Pass Rate</th>\n <th>Tests</th>\n <th>Duration</th>\n <th>Cost</th>\n <th>Tags</th>\n </tr>\n </thead>\n <tbody>\n @for (dp of getFilteredAnalyticsData(); track dp) {\n <tr (click)=\"openSuiteRun(dp.runId)\" class=\"clickable-row\">\n <td>{{ dp.date | date:'short' }}</td>\n <td>\n <span class=\"status-chip\" [ngClass]=\"'status-' + dp.status.toLowerCase()\">{{ dp.status }}</span>\n </td>\n <td>\n <div class=\"pass-rate-cell\">\n <div class=\"pass-rate-bar\" [style.width.%]=\"dp.passRate\" [ngClass]=\"{'high': dp.passRate >= 80, 'medium': dp.passRate >= 50 && dp.passRate < 80, 'low': dp.passRate < 50}\"></div>\n <span>{{ dp.passRate.toFixed(0) }}%</span>\n </div>\n </td>\n <td>{{ dp.passedTests }}/{{ dp.totalTests }}</td>\n <td>{{ formatDuration(dp.duration) }}</td>\n <td>{{ formatCost(dp.cost) }}</td>\n <td>\n <div class=\"tag-cell\">\n @for (tag of dp.tags.slice(0, 2); track tag) {\n <span class=\"tag-chip-table\">{{ tag }}</span>\n }\n @if (dp.tags.length > 2) {\n <span class=\"tag-more\">+{{ dp.tags.length - 2 }}</span>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n @if (getFilteredAnalyticsData().length === 0) {\n <mj-empty-state\n Variant=\"no-results\"\n Size=\"compact\"\n Title=\"No runs match the current filters.\" />\n }\n </div>\n }\n <!-- Matrix View -->\n @if (analyticsView === 'matrix') {\n <!-- Loading Matrix -->\n @if (loadingMatrix) {\n <div class=\"loading-state\">\n <mj-loading text=\"Loading test matrix...\"></mj-loading>\n </div>\n }\n <!-- Matrix Content -->\n @if (!loadingMatrix && matrixLoaded && matrixData.length > 0) {\n <div class=\"matrix-section\">\n <div class=\"matrix-header\">\n <h3><i class=\"fas fa-th\"></i> Test Results Matrix</h3>\n <div class=\"matrix-header-right\">\n <div class=\"matrix-filter-input\">\n <i class=\"fas fa-search\"></i>\n <input type=\"text\"\n placeholder=\"Filter tests...\"\n [value]=\"matrixTestFilter\"\n (input)=\"onMatrixFilterInput($event)\"\n class=\"filter-input\">\n @if (matrixTestFilter) {\n <button class=\"clear-filter-btn\" (click)=\"clearMatrixFilter()\" title=\"Clear filter\">\n <i class=\"fas fa-times\"></i>\n </button>\n }\n </div>\n <span class=\"matrix-run-count\">{{ matrixData.length }} runs \u00B7 {{ getUniqueTestsFromMatrix().length }} tests</span>\n <button mjButton (click)=\"exportMatrixToCSV()\" [disabled]=\"matrixData.length === 0\" title=\"Export matrix to CSV\">\n <i class=\"fas fa-download\"></i> Export\n </button>\n </div>\n </div>\n <div class=\"matrix-scroll-container\">\n <table class=\"test-matrix\">\n <thead>\n <tr>\n <th class=\"seq-header\" (click)=\"toggleMatrixSort('sequence')\" title=\"Sort by sequence\">\n #\n <i class=\"fas\" [ngClass]=\"matrixSortBy === 'sequence' ? (matrixSortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort'\"></i>\n </th>\n <th class=\"test-name-header\" (click)=\"toggleMatrixSort('name')\" title=\"Sort by name\">\n Test\n <i class=\"fas\" [ngClass]=\"matrixSortBy === 'name' ? (matrixSortAsc ? 'fa-sort-up' : 'fa-sort-down') : 'fa-sort'\"></i>\n </th>\n @for (run of matrixData; track run) {\n <th class=\"run-header\" (click)=\"openSuiteRun(run.runId)\" [title]=\"'Click to view suite run - ' + (run.date | date:'medium')\">\n <div class=\"run-header-content\">\n @if (run.tags.length > 0) {\n <div class=\"run-tags-header\">\n @for (tag of run.tags.slice(0, 2); track tag) {\n <span class=\"tag-chip-header\">{{ tag }}</span>\n }\n @if (run.tags.length > 2) {\n <span class=\"tag-more-header\">+{{ run.tags.length - 2 }}</span>\n }\n </div>\n }\n <div class=\"run-date\">{{ getRelativeTime(run.date) }}</div>\n <div class=\"run-pass-rate\" [ngClass]=\"{'high': run.passRate >= 80, 'medium': run.passRate >= 50 && run.passRate < 80, 'low': run.passRate < 50}\">\n {{ run.passRate.toFixed(0) }}%\n </div>\n </div>\n </th>\n }\n <th class=\"spacer-header\"></th>\n </tr>\n </thead>\n <tbody>\n @for (test of getUniqueTestsFromMatrix(); track test) {\n <tr\n [class.row-selected]=\"selectedMatrixTestId === test.testId\"\n (click)=\"selectMatrixRow(test.testId)\">\n <td class=\"seq-cell\">{{ test.sequence }}</td>\n <td class=\"test-name-cell\">\n <span class=\"test-name\" [title]=\"test.testName\">{{ test.testName }}</span>\n </td>\n @for (run of matrixData; track run) {\n <td class=\"result-cell\"\n [ngClass]=\"getMatrixCellClass(getTestResultForRun(run.runId, test.testId))\"\n [class.clickable]=\"getTestResultForRun(run.runId, test.testId)\"\n [class.cell-not-run]=\"!getTestResultForRun(run.runId, test.testId)\"\n [title]=\"getTestResultForRun(run.runId, test.testId)?.status + ' - Click to view test run' || 'Not Run'\"\n (click)=\"onMatrixCellClick(getTestResultForRun(run.runId, test.testId), $event)\">\n @if (getTestResultForRun(run.runId, test.testId); as result) {\n <div class=\"cell-eval-stack\">\n <!-- Status indicator -->\n @if (evalPreferences.showExecution) {\n <span class=\"cell-status\"\n [ngClass]=\"'status-' + result.status.toLowerCase()\"\n [class.cell-skipped-status]=\"result.status === 'Skipped' || result.status === 'Pending'\"\n [title]=\"getStatusTooltip(result.status)\">\n <i class=\"fas\"\n [class.fa-check]=\"result.status === 'Passed'\"\n [class.fa-times]=\"result.status === 'Failed'\"\n [class.fa-exclamation]=\"result.status === 'Error'\"\n [class.fa-hourglass-end]=\"result.status === 'Timeout'\"\n [class.fa-forward]=\"result.status === 'Skipped'\"\n [class.fa-spinner]=\"result.status === 'Running'\"\n [class.fa-clock]=\"result.status === 'Pending'\"></i>\n </span>\n }\n <!-- Human score - slashed icon if no feedback, colored by rating if has feedback -->\n @if (evalPreferences.showHuman && !result.humanRating) {\n <span class=\"cell-human no-feedback\"\n title=\"Human Review: No rating submitted yet\">\n <i class=\"fas fa-user-slash\"></i>\n </span>\n }\n @if (evalPreferences.showHuman && result.humanRating) {\n <span class=\"cell-human has-feedback\"\n [class.rating-low]=\"result.humanRating <= 4\"\n [class.rating-medium]=\"result.humanRating >= 5 && result.humanRating <= 6\"\n [class.rating-good]=\"result.humanRating >= 7 && result.humanRating <= 8\"\n [class.rating-excellent]=\"result.humanRating >= 9\"\n [title]=\"getHumanTooltip(result.humanRating, result.humanComments)\">\n <i class=\"fas fa-user\"></i>\n <span class=\"rating-value\">{{ result.humanRating }}</span>\n </span>\n }\n <!-- Auto score - colored by percentage -->\n @if (evalPreferences.showAuto && result.score != null) {\n <span class=\"cell-auto has-score\"\n [class.score-low]=\"result.score < 0.5\"\n [class.score-medium]=\"result.score >= 0.5 && result.score < 0.7\"\n [class.score-good]=\"result.score >= 0.7 && result.score < 0.85\"\n [class.score-excellent]=\"result.score >= 0.85\"\n [title]=\"'Auto Score: ' + (result.score * 100).toFixed(0) + '% automated evaluation'\">\n <i class=\"fas fa-robot\"></i>\n <span class=\"score-value\">{{ (result.score * 100).toFixed(0) }}</span>\n </span>\n }\n @if (evalPreferences.showAuto && result.score == null) {\n <span class=\"cell-auto no-score\"\n title=\"Auto Score: No automated score available\">\n <i class=\"fas fa-robot\"></i>\n </span>\n }\n </div>\n }\n @if (!getTestResultForRun(run.runId, test.testId)) {\n <span class=\"cell-not-run-indicator\">\n <i class=\"fas fa-minus\"></i>\n </span>\n }\n </td>\n }\n <td class=\"spacer-cell\"></td>\n </tr>\n }\n </tbody>\n <!-- Footer row with totals -->\n <tfoot>\n <tr class=\"totals-row\">\n <td class=\"seq-cell totals-label\"></td>\n <td class=\"test-name-cell totals-label\">\n <strong>Totals</strong>\n </td>\n @for (run of matrixData; track run) {\n <td class=\"result-cell totals-cell\">\n <div class=\"cell-eval-stack totals-stack\">\n <!-- Status totals -->\n @if (evalPreferences.showExecution) {\n <span class=\"totals-status\">\n <span class=\"pass-count\">{{ getRunPassedCount(run) }}/{{ getRunTotalCount(run) }}</span>\n </span>\n }\n <!-- Human totals -->\n @if (evalPreferences.showHuman) {\n <span class=\"totals-human\">\n @if (getRunHumanAvg(run) != null) {\n <span class=\"avg-label\">{{ getRunHumanAvg(run)?.toFixed(1) }}</span>\n }\n <span class=\"count-label\">({{ getRunHumanCount(run) }})</span>\n </span>\n }\n <!-- Auto totals -->\n @if (evalPreferences.showAuto) {\n <span class=\"totals-auto\">\n @if (getRunAutoAvg(run) != null) {\n <span class=\"avg-label\">{{ (getRunAutoAvg(run)! * 100).toFixed(0) }}%</span>\n }\n <span class=\"count-label\">({{ getRunAutoCount(run) }})</span>\n </span>\n }\n </div>\n </td>\n }\n <td class=\"spacer-cell\"></td>\n </tr>\n </tfoot>\n </table>\n </div>\n </div>\n }\n <!-- Empty Matrix State -->\n @if (!loadingMatrix && matrixLoaded && matrixData.length === 0) {\n <mj-empty-state\n Variant=\"no-results\"\n Icon=\"fa-solid fa-table-cells\"\n Title=\"No Matrix Data\"\n Message=\"No suite runs match the current filters.\" />\n }\n }\n <!-- Chart View -->\n @if (analyticsView === 'chart') {\n <!-- Loading Chart -->\n @if (loadingMatrix) {\n <div class=\"loading-state\">\n <mj-loading text=\"Loading chart data...\"></mj-loading>\n </div>\n }\n <!-- Chart Content -->\n @if (!loadingMatrix && matrixLoaded && matrixData.length > 0) {\n <div class=\"chart-section\">\n <div class=\"chart-header\">\n <h3><i class=\"fas fa-project-diagram\"></i> Test Results Flow</h3>\n <div class=\"chart-legend\">\n <span class=\"legend-item chart-passed\"><i class=\"fas fa-check\"></i> Passed</span>\n <span class=\"legend-item chart-failed\"><i class=\"fas fa-times\"></i> Failed</span>\n <span class=\"legend-item chart-error\"><i class=\"fas fa-exclamation\"></i> Error</span>\n <span class=\"legend-item chart-skipped\"><i class=\"fas fa-forward\"></i> Skipped</span>\n </div>\n </div>\n <div class=\"chart-container\">\n <div #chartContainer class=\"d3-chart\"></div>\n </div>\n <div class=\"chart-info\">\n <i class=\"fas fa-info-circle\"></i>\n Interactive visualization showing test results across {{ matrixData.length }} runs.\n Hover over elements for details, click nodes to navigate.\n </div>\n </div>\n }\n <!-- Empty Chart State -->\n @if (!loadingMatrix && matrixLoaded && matrixData.length === 0) {\n <mj-empty-state\n Variant=\"no-results\"\n Icon=\"fa-solid fa-diagram-project\"\n Title=\"No Chart Data\"\n Message=\"No suite runs match the current filters.\" />\n }\n }\n }\n <!-- Empty State -->\n @if (!loadingAnalytics && analyticsLoaded && analyticsData.length === 0) {\n <mj-empty-state\n Icon=\"fa-solid fa-chart-line\"\n Title=\"No Analytics Data\"\n Message=\"Run this suite to start collecting analytics data.\"\n ActionText=\"Run Suite Now\"\n ActionIcon=\"fa-solid fa-play\"\n (Action)=\"runSuite()\" />\n }\n </div>\n }\n\n <!-- Compare Tab -->\n @if (activeTab === 'compare') {\n <div class=\"compare-tab\">\n <!-- Run Selection -->\n <div class=\"compare-selection\">\n <div class=\"compare-run-selector\">\n <h4>Run A (Baseline)</h4>\n @if (!loadingRuns && suiteRuns.length > 0) {\n <div class=\"run-selector-list\">\n @for (run of suiteRuns; track run) {\n <div class=\"run-selector-item\"\n [class.selected]=\"IsCompareRunA(run)\"\n (click)=\"selectCompareRunA(run)\">\n <div class=\"selector-status\" [style.background-color]=\"getRunStatusColor(run.Status)\"></div>\n <div class=\"selector-content\">\n <div class=\"selector-date\">{{ run.StartedAt | date:'short' }}</div>\n <div class=\"selector-rate\">{{ getPassRate(run).toFixed(0) }}% pass</div>\n </div>\n @if (getRunTags(run).length > 0) {\n <div class=\"selector-tags\">\n @for (tag of getRunTags(run).slice(0, 2); track tag) {\n <span class=\"tag-mini\">{{ tag }}</span>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n @if (compareRunA) {\n <div class=\"selected-run-preview\">\n <div class=\"preview-header\">\n <span class=\"preview-label\">Selected:</span>\n <button class=\"clear-btn\" (click)=\"compareRunA = null; compareResults = []\">Clear</button>\n </div>\n <div class=\"preview-details\">\n <span>{{ compareRunA.StartedAt | date:'medium' }}</span>\n <span class=\"preview-rate\">{{ getPassRate(compareRunA).toFixed(1) }}%</span>\n </div>\n </div>\n }\n </div>\n <div class=\"compare-vs\"><i class=\"fas fa-exchange-alt\"></i></div>\n <div class=\"compare-run-selector\">\n <h4>Run B (Compare)</h4>\n @if (!loadingRuns && suiteRuns.length > 0) {\n <div class=\"run-selector-list\">\n @for (run of suiteRuns; track run) {\n <div class=\"run-selector-item\"\n [class.selected]=\"IsCompareRunB(run)\"\n [class.disabled]=\"IsCompareRunA(run)\"\n (click)=\"!IsCompareRunA(run) && selectCompareRunB(run)\">\n <div class=\"selector-status\" [style.background-color]=\"getRunStatusColor(run.Status)\"></div>\n <div class=\"selector-content\">\n <div class=\"selector-date\">{{ run.StartedAt | date:'short' }}</div>\n <div class=\"selector-rate\">{{ getPassRate(run).toFixed(0) }}% pass</div>\n </div>\n @if (getRunTags(run).length > 0) {\n <div class=\"selector-tags\">\n @for (tag of getRunTags(run).slice(0, 2); track tag) {\n <span class=\"tag-mini\">{{ tag }}</span>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n @if (compareRunB) {\n <div class=\"selected-run-preview\">\n <div class=\"preview-header\">\n <span class=\"preview-label\">Selected:</span>\n <button class=\"clear-btn\" (click)=\"compareRunB = null; compareResults = []\">Clear</button>\n </div>\n <div class=\"preview-details\">\n <span>{{ compareRunB.StartedAt | date:'medium' }}</span>\n <span class=\"preview-rate\">{{ getPassRate(compareRunB).toFixed(1) }}%</span>\n </div>\n </div>\n }\n </div>\n </div>\n <!-- Comparison Results -->\n @if (compareRunA && compareRunB && !loadingCompare) {\n <div class=\"compare-results\">\n <!-- Summary Cards -->\n <div class=\"compare-summary\">\n <div class=\"compare-summary-card\">\n <div class=\"summary-label\">Pass Rate Change</div>\n <div class=\"summary-value\" [ngClass]=\"{'positive': getComparePassRateDiff()! > 0, 'negative': getComparePassRateDiff()! < 0}\">\n <i class=\"fas\" [ngClass]=\"{'fa-arrow-up': getComparePassRateDiff()! > 0, 'fa-arrow-down': getComparePassRateDiff()! < 0, 'fa-minus': getComparePassRateDiff() === 0}\"></i>\n {{ getComparePassRateDiff()! > 0 ? '+' : '' }}{{ getComparePassRateDiff()?.toFixed(1) }}%\n </div>\n </div>\n <div class=\"compare-summary-card\">\n <div class=\"summary-label\">Duration Change</div>\n <div class=\"summary-value\" [ngClass]=\"{'positive': getCompareDurationDiff()! < 0, 'negative': getCompareDurationDiff()! > 0}\">\n <i class=\"fas\" [ngClass]=\"{'fa-arrow-down': getCompareDurationDiff()! < 0, 'fa-arrow-up': getCompareDurationDiff()! > 0, 'fa-minus': getCompareDurationDiff() === 0}\"></i>\n {{ formatDuration(getAbsCompareDurationDiff()) }}\n </div>\n </div>\n <div class=\"compare-summary-card improved\">\n <div class=\"summary-label\">Improved</div>\n <div class=\"summary-value\">{{ getCompareImprovedCount() }}</div>\n </div>\n <div class=\"compare-summary-card regressed\">\n <div class=\"summary-label\">Regressed</div>\n <div class=\"summary-value\">{{ getCompareRegressedCount() }}</div>\n </div>\n </div>\n <!-- Detailed Comparison Table -->\n <div class=\"compare-table-section\">\n <h3><i class=\"fas fa-list\"></i> Test-by-Test Comparison</h3>\n <div class=\"compare-table-wrapper\">\n <table class=\"compare-table\">\n <thead>\n <tr>\n <th>Test</th>\n <th>Run A Status</th>\n <th>Run B Status</th>\n <th>Score Diff</th>\n <th>Duration Diff</th>\n <th>Change</th>\n </tr>\n </thead>\n <tbody>\n @for (result of compareResults; track result) {\n <tr [ngClass]=\"{'improved': result.runA && result.runB && result.runA.status !== 'Passed' && result.runB.status === 'Passed', 'regressed': result.runA && result.runB && result.runA.status === 'Passed' && result.runB.status !== 'Passed'}\">\n <td class=\"test-name-cell\">{{ result.testName }}</td>\n <td>\n @if (result.runA) {\n <span class=\"status-chip\" [ngClass]=\"'status-' + result.runA.status.toLowerCase()\">{{ result.runA.status }}</span>\n }\n @if (!result.runA) {\n <span class=\"status-chip status-missing\">N/A</span>\n }\n </td>\n <td>\n @if (result.runB) {\n <span class=\"status-chip\" [ngClass]=\"'status-' + result.runB.status.toLowerCase()\">{{ result.runB.status }}</span>\n }\n @if (!result.runB) {\n <span class=\"status-chip status-missing\">N/A</span>\n }\n </td>\n <td>\n @if (result.scoreDiff != null) {\n <span [ngClass]=\"{'positive': result.scoreDiff > 0, 'negative': result.scoreDiff < 0}\">\n {{ result.scoreDiff > 0 ? '+' : '' }}{{ (result.scoreDiff * 100).toFixed(1) }}%\n </span>\n }\n @if (result.scoreDiff == null) {\n <span class=\"muted\">-</span>\n }\n </td>\n <td>\n @if (result.durationDiff != null) {\n <span [ngClass]=\"{'positive': result.durationDiff < 0, 'negative': result.durationDiff > 0}\">\n {{ result.durationDiff > 0 ? '+' : '' }}{{ result.durationDiff.toFixed(1) }}s\n </span>\n }\n @if (result.durationDiff == null) {\n <span class=\"muted\">-</span>\n }\n </td>\n <td>\n @if (result.runA && result.runB && result.runA.status !== 'Passed' && result.runB.status === 'Passed') {\n <span class=\"change-indicator improved\">\n <i class=\"fas fa-arrow-up\"></i> Fixed\n </span>\n }\n @if (result.runA && result.runB && result.runA.status === 'Passed' && result.runB.status !== 'Passed') {\n <span class=\"change-indicator regressed\">\n <i class=\"fas fa-arrow-down\"></i> Broke\n </span>\n }\n @if (!result.statusChanged) {\n <span class=\"change-indicator unchanged\">\n <i class=\"fas fa-minus\"></i>\n </span>\n }\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n </div>\n </div>\n }\n <!-- Loading State for Compare -->\n @if (loadingCompare) {\n <div class=\"loading-state\">\n <mj-loading text=\"Loading comparison data...\"></mj-loading>\n </div>\n }\n <!-- Empty State -->\n @if (!compareRunA || !compareRunB) {\n <mj-empty-state\n Icon=\"fa-solid fa-scale-balanced\"\n Title=\"Select Two Runs to Compare\"\n Message=\"Choose a baseline run (A) and a comparison run (B) from the lists above to see a detailed side-by-side comparison.\" />\n }\n <!-- No Runs State -->\n @if (runsLoaded && suiteRuns.length < 2) {\n <mj-empty-state\n Icon=\"fa-solid fa-scale-balanced\"\n Title=\"Not Enough Runs to Compare\"\n Message=\"You need at least 2 suite runs to use the comparison feature.\"\n ActionText=\"Run Suite Now\"\n ActionIcon=\"fa-solid fa-play\"\n (Action)=\"runSuite()\" />\n }\n </div>\n }\n </div>\n\n <!-- Keyboard Shortcuts Toggle Button -->\n <button class=\"shortcuts-toggle\" (click)=\"toggleShortcuts()\" [title]=\"showShortcuts ? 'Hide keyboard shortcuts' : 'Show keyboard shortcuts'\">\n <i class=\"fas fa-keyboard\"></i>\n </button>\n\n <!-- Keyboard Shortcuts Hint (Desktop Only) -->\n @if (showShortcuts) {\n <div class=\"keyboard-shortcuts\">\n <div class=\"shortcuts-header\">\n <i class=\"fas fa-keyboard\"></i>\n Shortcuts\n <button class=\"shortcuts-close\" (click)=\"toggleShortcuts()\" title=\"Hide shortcuts\">\n <i class=\"fas fa-times\"></i>\n </button>\n </div>\n <div class=\"shortcut-list\">\n <div class=\"shortcut-item\">\n <span>Save changes</span>\n <span class=\"shortcut-keys\"><kbd>Cmd</kbd><kbd>S</kbd></span>\n </div>\n <div class=\"shortcut-item\">\n <span>Refresh</span>\n <span class=\"shortcut-keys\"><kbd>Cmd</kbd><kbd>R</kbd></span>\n </div>\n <div class=\"shortcut-item\">\n <span>Run Suite</span>\n <span class=\"shortcut-keys\"><kbd>Cmd</kbd><kbd>Enter</kbd></span>\n </div>\n <div class=\"shortcut-item\">\n <span>Switch Tabs</span>\n <span class=\"shortcut-keys\"><kbd>1</kbd>-<kbd>5</kbd></span>\n </div>\n </div>\n </div>\n }\n\n <!-- Slide Panel for Suite Execution -->\n @if (testingDialogService.IsPanelOpen) {\n <mj-slide-panel\n Mode=\"slide\"\n [Title]=\"testingDialogService.PanelOptions?.suiteId ? 'Suite Execution' : 'Run Suite'\"\n [Resizable]=\"true\"\n (Closed)=\"OnPanelClosed()\">\n <app-test-run-dialog\n [PanelMode]=\"true\"\n [selectedTestId]=\"testingDialogService.PanelOptions?.testId ?? null\"\n [selectedSuiteId]=\"testingDialogService.PanelOptions?.suiteId ?? null\"\n [runMode]=\"testingDialogService.PanelOptions?.mode ?? 'suite'\"\n (PanelClose)=\"OnPanelClosed()\">\n </app-test-run-dialog>\n </mj-slide-panel>\n }\n\n <!-- Add Tests Picker Dialog -->\n @if (showAddTestsDialog) {\n <div class=\"picker-backdrop\" (click)=\"closeAddTestsDialog()\"></div>\n <div class=\"picker-dialog\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"add-tests-dialog-title\" (click)=\"$event.stopPropagation()\">\n <div class=\"picker-header\">\n <h2 id=\"add-tests-dialog-title\" class=\"picker-title\">\n <i class=\"fas fa-plus-circle\"></i> Add tests to suite\n </h2>\n <button type=\"button\" class=\"picker-close\" (click)=\"closeAddTestsDialog()\" [disabled]=\"isAddingTests\" aria-label=\"Close\">\n <i class=\"fas fa-times\"></i>\n </button>\n </div>\n\n <div class=\"picker-search\">\n <i class=\"fas fa-search picker-search-icon\"></i>\n <input\n #pickerSearch\n type=\"text\"\n class=\"picker-search-input\"\n placeholder=\"Search tests by name, description, or type...\"\n [(ngModel)]=\"addTestsSearch\"\n autofocus\n />\n @if (addTestsSearch) {\n <button type=\"button\" class=\"picker-search-clear\" (click)=\"addTestsSearch = ''\" aria-label=\"Clear search\">\n <i class=\"fas fa-times\"></i>\n </button>\n }\n </div>\n\n <div class=\"picker-toolbar\">\n <span class=\"picker-toolbar-status\">\n @if (selectedTestIdsToAdd.size > 0) {\n <strong>{{ selectedTestIdsToAdd.size }}</strong> selected\n } @else if (!loadingAvailableTests) {\n {{ filteredAvailableTests.length }} test{{ filteredAvailableTests.length === 1 ? '' : 's' }} available\n }\n </span>\n <div class=\"picker-toolbar-actions\">\n <button type=\"button\" class=\"picker-link-btn\" (click)=\"selectAllFiltered()\" [disabled]=\"loadingAvailableTests || filteredAvailableTests.length === 0\">\n Select all\n </button>\n <button type=\"button\" class=\"picker-link-btn\" (click)=\"clearAddSelection()\" [disabled]=\"selectedTestIdsToAdd.size === 0\">\n Clear\n </button>\n </div>\n </div>\n\n <div class=\"picker-body\">\n @if (loadingAvailableTests) {\n <div class=\"picker-loading\">\n <mj-loading text=\"Loading tests...\"></mj-loading>\n </div>\n } @else if (filteredAvailableTests.length === 0) {\n @if (availableTests.length === 0) {\n <mj-empty-state\n Variant=\"success\"\n Size=\"compact\"\n Title=\"Everything's already in the suite\"\n Message=\"This suite contains every available test. Create a new test first to add more.\" />\n } @else {\n <mj-empty-state\n Variant=\"no-results\"\n Size=\"compact\"\n Title=\"No matches\"\n [Message]=\"AddTestsNoMatchMessage\" />\n }\n } @else {\n <ul class=\"picker-list\">\n @for (t of filteredAvailableTests; track t.ID) {\n <li\n class=\"picker-row\"\n [class.picker-row--selected]=\"isAddSelected(t.ID)\"\n (click)=\"toggleAddSelection(t.ID)\"\n role=\"checkbox\"\n [attr.aria-checked]=\"isAddSelected(t.ID)\"\n tabindex=\"0\"\n >\n <span class=\"picker-check\">\n @if (isAddSelected(t.ID)) {\n <i class=\"fas fa-check\"></i>\n }\n </span>\n <div class=\"picker-row-main\">\n <div class=\"picker-row-name\">{{ t.Name }}</div>\n @if (t.Description) {\n <div class=\"picker-row-desc\">{{ t.Description }}</div>\n }\n </div>\n <div class=\"picker-row-meta\">\n @if (t.Type) {\n <span class=\"picker-row-pill\">{{ t.Type }}</span>\n }\n <span class=\"picker-row-pill picker-row-pill--status\" [attr.data-status]=\"t.Status?.toLowerCase()\">{{ t.Status }}</span>\n </div>\n </li>\n }\n </ul>\n }\n </div>\n\n <div class=\"picker-footer\">\n <button\n type=\"button\"\n class=\"picker-btn picker-btn--primary\"\n (click)=\"confirmAddTests()\"\n [disabled]=\"selectedTestIdsToAdd.size === 0 || isAddingTests\"\n >\n @if (isAddingTests) {\n <i class=\"fas fa-spinner fa-spin\"></i> Adding\u2026\n } @else {\n <i class=\"fas fa-check\"></i>\n @if (selectedTestIdsToAdd.size === 0) {\n Add tests\n } @else if (selectedTestIdsToAdd.size === 1) {\n Add 1 test\n } @else {\n Add {{ selectedTestIdsToAdd.size }} tests\n }\n }\n </button>\n <button\n type=\"button\"\n class=\"picker-btn picker-btn--ghost\"\n (click)=\"closeAddTestsDialog()\"\n [disabled]=\"isAddingTests\"\n >\n Cancel\n </button>\n </div>\n </div>\n }\n\n <!-- Sticky Save Bar \u2014 always in the DOM so CSS transitions can fire reliably.\n Visibility is controlled by the .save-bar--visible class, driven by isDirty. -->\n <div\n class=\"save-bar\"\n [class.save-bar--visible]=\"isDirty\"\n [attr.aria-hidden]=\"!isDirty\"\n role=\"region\"\n aria-label=\"Unsaved changes\"\n >\n <div class=\"save-bar-message\">\n <span class=\"save-bar-dot\" aria-hidden=\"true\">\u25CF</span>\n <span class=\"save-bar-text\">\n <strong>Unsaved changes</strong>\n <span class=\"save-bar-fields\">\n @if (dirtyFieldNames.length === 1) {\n {{ dirtyFieldNames[0] }} edited\n } @else if (dirtyFieldNames.length > 1) {\n {{ dirtyFieldNames.length }} fields edited\n }\n </span>\n </span>\n </div>\n <div class=\"save-bar-actions\">\n <button\n type=\"button\"\n class=\"save-bar-btn save-bar-btn--primary\"\n (click)=\"saveChanges()\"\n [disabled]=\"isSaving || !isDirty\"\n [attr.tabindex]=\"isDirty ? 0 : -1\"\n title=\"Save changes (\u2318S)\"\n >\n @if (isSaving) {\n <i class=\"fas fa-spinner fa-spin\"></i> Saving\u2026\n } @else {\n <i class=\"fas fa-check\"></i> Save changes\n <span class=\"save-bar-kbd\"><kbd>\u2318</kbd><kbd>S</kbd></span>\n }\n </button>\n <button\n type=\"button\"\n class=\"save-bar-btn save-bar-btn--ghost\"\n (click)=\"discardChanges()\"\n [disabled]=\"isSaving || !isDirty\"\n [attr.tabindex]=\"isDirty ? 0 : -1\"\n title=\"Discard changes\"\n >\n <i class=\"fas fa-undo\"></i> Discard\n </button>\n </div>\n </div>\n</div>\n", styles: ["/* ===========================\n Test Suite Form - World-Class UX\n =========================== */\n\n/* CSS Custom Properties for Theming - using :host for Angular encapsulation */\n:host {\n --test-primary: var(--mj-brand-primary);\n --test-primary-light: var(--mj-brand-primary);\n --test-success: var(--mj-status-success);\n --test-error: var(--mj-status-error);\n --test-warning: var(--mj-status-warning);\n --test-disabled: var(--mj-text-muted);\n --test-bg: var(--mj-bg-surface-card);\n --test-surface: var(--mj-bg-surface);\n --test-border: var(--mj-border-default);\n --test-text: var(--mj-text-primary);\n --test-text-secondary: var(--mj-text-secondary);\n --test-text-muted: var(--mj-text-disabled);\n --test-radius-sm: 6px;\n --test-radius-md: 10px;\n --test-radius-lg: 16px;\n --test-shadow-sm: var(--mj-shadow-sm);\n --test-shadow-md: var(--mj-shadow-md);\n --test-shadow-lg: var(--mj-shadow-lg);\n --test-transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n display: block;\n height: 100%;\n}\n\n.test-suite-form {\n position: relative;\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--test-bg);\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n\n/* Breadcrumb */\n.breadcrumb {\n margin-bottom: 16px;\n}\n\n.breadcrumb ol {\n display: flex;\n align-items: center;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n font-size: 13px;\n}\n\n.breadcrumb li {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.breadcrumb a {\n color: var(--test-primary);\n text-decoration: none;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 8px;\n border-radius: 6px;\n transition: background 0.15s;\n}\n\n.breadcrumb a:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n text-decoration: none;\n}\n\n.breadcrumb .separator {\n font-size: 10px;\n color: var(--test-text-muted);\n margin: 0 4px;\n}\n\n.breadcrumb .current {\n color: var(--test-text-secondary);\n font-weight: 500;\n}\n\n.breadcrumb-text {\n max-width: 200px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Header */\n.suite-header {\n background: var(--test-surface);\n border-bottom: 1px solid var(--test-border);\n padding: 20px;\n}\n\n.header-content {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n margin-bottom: 16px;\n gap: 16px;\n}\n\n.header-left {\n display: flex;\n gap: 16px;\n flex: 1;\n min-width: 0;\n}\n\n.suite-icon {\n width: 56px;\n height: 56px;\n border-radius: var(--test-radius-md);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-inverse);\n font-size: 24px;\n flex-shrink: 0;\n box-shadow: var(--test-shadow-md);\n transition: var(--test-transition);\n}\n\n.suite-icon:hover { transform: scale(1.05); }\n\n.suite-info { flex: 1; min-width: 0; }\n\n.suite-info h1 {\n margin: 0 0 8px 0;\n font-size: clamp(18px, 4vw, 24px);\n font-weight: 700;\n color: var(--test-text);\n word-wrap: break-word;\n}\n\n.suite-meta {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.status-badge {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 14px;\n border-radius: 20px;\n color: var(--mj-text-inverse);\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n}\n\n.status-badge.status-active { background: var(--test-success); }\n.status-badge.status-disabled { background: var(--test-disabled); }\n.status-badge.status-pending { background: var(--test-warning); }\n\n.status-badge-inline {\n display: inline-flex;\n padding: 2px 10px;\n border-radius: 10px;\n color: var(--mj-text-inverse);\n font-size: 11px;\n font-weight: 600;\n}\n\n.status-badge-inline.status-active { background: var(--test-success); }\n.status-badge-inline.status-disabled { background: var(--test-disabled); }\n.status-badge-inline.status-pending { background: var(--test-warning); }\n\n.test-count {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n color: var(--test-text-secondary);\n padding: 4px 10px;\n background: var(--test-bg);\n border-radius: var(--test-radius-sm);\n}\n\n.header-actions {\n display: flex;\n gap: 8px;\n flex-shrink: 0;\n}\n\n.suite-description {\n padding: 16px;\n background: var(--mj-bg-surface-card);\n border-radius: var(--test-radius-md);\n border: 1px solid var(--test-border);\n}\n\n.suite-description p {\n margin: 0;\n color: var(--test-text-secondary);\n line-height: 1.6;\n font-size: 14px;\n}\n\n/* Tabs */\n.tabs-container {\n background: var(--test-surface);\n border-bottom: 1px solid var(--test-border);\n position: sticky;\n top: 0;\n z-index: 10;\n}\n\n.tabs {\n display: flex;\n padding: 0 20px;\n overflow-x: auto;\n scrollbar-width: none;\n}\n\n.tabs::-webkit-scrollbar { display: none; }\n\n.tab {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 14px 18px;\n border: none;\n background: transparent;\n border-bottom: 3px solid transparent;\n color: var(--test-text-secondary);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: var(--test-transition);\n white-space: nowrap;\n}\n\n.tab:hover {\n color: var(--test-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n}\n\n.tab.active {\n color: var(--test-primary);\n border-bottom-color: var(--test-primary);\n font-weight: 600;\n}\n\n.tab-badge {\n background: var(--test-border);\n color: var(--test-text-secondary);\n padding: 2px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n}\n\n.tab.active .tab-badge {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n color: var(--test-primary);\n}\n\n.tab-shortcut {\n font-size: 10px;\n color: var(--test-text-muted);\n background: var(--test-bg);\n padding: 2px 6px;\n border-radius: 4px;\n font-weight: 600;\n}\n\n/* Tab Content */\n.tab-content {\n flex: 1;\n overflow-y: auto;\n padding: 20px;\n}\n\n/* Overview Tab */\n.overview-tab {\n display: flex;\n flex-direction: column;\n gap: 20px;\n animation: fadeIn 0.3s ease-out;\n}\n\n@keyframes fadeIn {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n.info-section, .config-section {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 24px;\n box-shadow: var(--test-shadow-sm);\n}\n\n.info-section h3, .config-section h3 {\n margin: 0 0 20px 0;\n font-size: 18px;\n font-weight: 700;\n color: var(--test-text);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.info-section h3 i, .config-section h3 i {\n color: var(--test-primary);\n}\n\n.info-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 16px;\n}\n\n.info-item {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.info-label {\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--test-text-muted);\n}\n\n.info-value {\n font-size: 14px;\n color: var(--test-text);\n font-weight: 500;\n}\n\n.config-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));\n gap: 20px;\n}\n\n.config-item {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.config-item label {\n font-size: 12px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--test-text-muted);\n}\n\n.config-input {\n padding: 10px 14px;\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-sm);\n font-size: 14px;\n transition: var(--test-transition);\n}\n\n.config-input:focus {\n outline: none;\n border-color: var(--test-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.config-hint {\n font-size: 11px;\n color: var(--test-text-muted);\n}\n\n/* Tests Tab */\n.tests-tab, .runs-tab {\n animation: fadeIn 0.3s ease-out;\n}\n\n.loading-state { padding: 0; }\n\n.skeleton-list {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.skeleton-card {\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 16px;\n background: var(--test-surface);\n border-radius: var(--test-radius-md);\n border: 1px solid var(--test-border);\n}\n\n.skeleton-sequence {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: linear-gradient(90deg, var(--mj-border-default) 25%, var(--mj-bg-surface-sunken) 50%, var(--mj-border-default) 75%);\n background-size: 200% 100%;\n animation: shimmer 1.5s infinite;\n}\n\n.skeleton-icon {\n width: 40px;\n height: 40px;\n border-radius: var(--test-radius-md);\n background: linear-gradient(90deg, var(--mj-border-default) 25%, var(--mj-bg-surface-sunken) 50%, var(--mj-border-default) 75%);\n background-size: 200% 100%;\n animation: shimmer 1.5s infinite;\n}\n\n.skeleton-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.skeleton-line {\n height: 14px;\n border-radius: 4px;\n background: linear-gradient(90deg, var(--mj-border-default) 25%, var(--mj-bg-surface-sunken) 50%, var(--mj-border-default) 75%);\n background-size: 200% 100%;\n animation: shimmer 1.5s infinite;\n}\n\n.skeleton-line.wide { width: 70%; }\n.skeleton-line.narrow { width: 40%; }\n\n@keyframes shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n}\n\n.tests-list, .runs-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.test-item, .run-item {\n display: flex;\n align-items: center;\n gap: 14px;\n padding: 16px;\n background: var(--test-surface);\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-md);\n cursor: pointer;\n transition: var(--test-transition);\n}\n\n.test-item:hover, .run-item:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n border-color: var(--test-primary-light);\n transform: translateX(4px);\n}\n\n.test-sequence {\n width: 32px;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--test-bg);\n border-radius: 50%;\n font-size: 14px;\n font-weight: 700;\n color: var(--test-text-secondary);\n flex-shrink: 0;\n}\n\n.test-icon, .run-icon {\n width: 40px;\n height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--test-radius-md);\n color: var(--mj-text-inverse);\n font-size: 18px;\n flex-shrink: 0;\n box-shadow: var(--test-shadow-sm);\n}\n\n.test-icon {\n background: var(--test-primary);\n}\n\n.test-content, .run-content { flex: 1; min-width: 0; }\n\n.test-name, .run-header {\n font-size: 14px;\n font-weight: 600;\n color: var(--test-text);\n margin-bottom: 4px;\n}\n\n.run-header {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.run-id { font-weight: 600; }\n\n.run-status {\n font-size: 12px;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.test-status, .run-meta {\n display: flex;\n gap: 12px;\n font-size: 12px;\n color: var(--test-text-secondary);\n}\n\n.test-status span, .run-meta span {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n}\n\n.test-item > i, .run-item > i {\n color: var(--test-text-muted);\n font-size: 14px;\n transition: var(--test-transition);\n}\n\n.test-item:hover > i, .run-item:hover > i {\n color: var(--test-primary);\n transform: translateX(2px);\n}\n\n/* Keyboard Shortcuts */\n/* Keyboard shortcuts toggle button - visible when shortcuts are hidden */\n.shortcuts-toggle {\n position: fixed;\n bottom: 20px;\n right: 20px;\n width: 36px;\n height: 36px;\n border-radius: 50%;\n background: var(--test-surface);\n border: 1px solid var(--test-border);\n box-shadow: var(--test-shadow-md);\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--test-text-secondary);\n font-size: 14px;\n z-index: 99;\n transition: var(--test-transition);\n opacity: 0.7;\n}\n\n.shortcuts-toggle:hover {\n opacity: 1;\n transform: scale(1.1);\n color: var(--test-primary);\n border-color: var(--test-primary);\n}\n\n.keyboard-shortcuts {\n position: fixed;\n bottom: 20px;\n right: 20px;\n background: var(--test-surface);\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-md);\n padding: 12px 16px;\n box-shadow: var(--test-shadow-lg);\n font-size: 12px;\n z-index: 100;\n max-width: 260px;\n}\n\n.shortcuts-header {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 10px;\n padding-bottom: 8px;\n border-bottom: 1px solid var(--test-border);\n font-weight: 600;\n color: var(--test-text);\n}\n\n.shortcuts-close {\n margin-left: auto;\n background: none;\n border: none;\n cursor: pointer;\n color: var(--test-text-muted);\n font-size: 12px;\n padding: 2px 4px;\n border-radius: 4px;\n transition: var(--test-transition);\n}\n\n.shortcuts-close:hover {\n color: var(--test-text);\n background: var(--test-border);\n}\n\n.shortcut-list {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.shortcut-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n color: var(--test-text-secondary);\n}\n\n.shortcut-keys {\n display: flex;\n gap: 4px;\n}\n\n.shortcut-keys kbd {\n background: var(--test-bg);\n border: 1px solid var(--test-border);\n border-radius: 4px;\n padding: 2px 6px;\n font-size: 11px;\n color: var(--test-text);\n}\n\n/* Responsive */\n@media (max-width: 1024px) {\n .keyboard-shortcuts, .shortcuts-toggle { display: none; }\n}\n\n@media (max-width: 768px) {\n .suite-header { padding: 16px; }\n\n .header-content {\n flex-direction: column;\n gap: 16px;\n }\n\n .header-actions {\n width: 100%;\n justify-content: stretch;\n }\n\n .header-actions button { flex: 1; }\n\n .tab-shortcut { display: none; }\n\n .info-grid { grid-template-columns: 1fr; }\n\n .test-item, .run-item { padding: 14px; }\n}\n\n@media (max-width: 480px) {\n .suite-icon {\n width: 40px;\n height: 40px;\n font-size: 18px;\n }\n\n .suite-info h1 { font-size: 16px; }\n\n .tab-badge { display: none; }\n\n .test-sequence { display: none; }\n}\n\n@media (hover: none) and (pointer: coarse) {\n .test-item:active, .run-item:active {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n transform: scale(0.98);\n }\n\n .tab { min-height: 48px; }\n .test-item, .run-item { min-height: 64px; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n *, *::before, *::after {\n animation-duration: 0.01ms !important;\n transition-duration: 0.01ms !important;\n }\n}\n\n@media print {\n .header-actions, .tabs-container, .keyboard-shortcuts {\n display: none !important;\n }\n}\n\n/* ===========================\n Tags UI\n =========================== */\n.run-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 8px;\n}\n\n.tag-chip {\n display: inline-flex;\n align-items: center;\n padding: 3px 10px;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 12px;\n font-size: 11px;\n font-weight: 500;\n color: var(--mj-brand-primary-hover);\n}\n\n.tag-mini {\n display: inline-flex;\n align-items: center;\n padding: 2px 6px;\n background: var(--mj-bg-surface-sunken);\n border-radius: 4px;\n font-size: 10px;\n font-weight: 500;\n color: var(--mj-text-muted);\n}\n\n.tag-more {\n font-size: 10px;\n color: var(--mj-text-disabled);\n font-weight: 500;\n}\n\n/* Evaluation metrics row for Runs list */\n.run-eval-metrics {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-top: 8px;\n}\n\n.eval-metric {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 3px 8px;\n border-radius: 6px;\n font-size: 11px;\n font-weight: 500;\n}\n\n.eval-metric.status {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n}\n\n.eval-metric.status.status-completed {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n}\n\n.eval-metric.status.status-failed {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--mj-status-error);\n}\n\n.eval-metric.status.status-running {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary-hover);\n}\n\n.eval-metric.status.status-pending {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.eval-metric.human {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n border: 1px solid var(--mj-status-warning);\n color: var(--mj-status-warning);\n}\n\n.eval-metric.human .eval-pending {\n font-size: 9px;\n color: var(--mj-status-warning);\n}\n\n.eval-metric.auto {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n color: var(--mj-brand-primary-hover);\n}\n\n.eval-metric.auto.high {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-status-success) 30%, transparent);\n color: var(--mj-status-success);\n}\n\n.eval-metric.auto.medium {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-status-warning) 30%, transparent);\n color: var(--mj-status-warning);\n}\n\n.eval-metric.auto.low {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-status-error) 30%, transparent);\n color: var(--mj-status-error);\n}\n\n.tag-cell {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-wrap: wrap;\n}\n\n.tag-chip-table {\n display: inline-block;\n padding: 3px 8px;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n color: var(--mj-brand-primary-hover);\n border-radius: 10px;\n font-size: 10px;\n font-weight: 600;\n white-space: nowrap;\n max-width: 80px;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ===========================\n Analytics Tab\n =========================== */\n.analytics-tab {\n animation: fadeIn 0.3s ease-out;\n}\n\n/* Collapsible filters */\n/* Filters disclosure migrated to <mj-accordion-panel>. The bespoke\n .analytics-filters wrapper card chrome, the .filters-header bar (+ hover),\n and the .filters-header > i chevron are now owned by the accordion and were\n removed. The title-slot layout (.filters-title flex row) and font are now\n provided by the accordion title + Size=\"sm\"; the collapsed summary uses the\n shared .mj-accordion-badge (--muted --right). Only the body content\n (.filters-content) styling is kept below. */\n\n.filters-content {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 12px 16px 16px 16px;\n border-top: 1px solid var(--test-border);\n}\n\n.filter-group {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.filter-group label {\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--test-text-muted);\n}\n\n.filter-buttons {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.filter-btn {\n padding: 8px 16px;\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-sm);\n background: var(--test-surface);\n color: var(--test-text-secondary);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: var(--test-transition);\n}\n\n.filter-btn:hover {\n border-color: var(--test-primary);\n color: var(--test-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n}\n\n.filter-btn.active {\n background: var(--test-primary);\n border-color: var(--test-primary);\n color: var(--mj-text-inverse);\n}\n\n.filter-btn.tag-btn {\n padding: 6px 12px;\n font-size: 12px;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.filter-btn.tag-btn i.fa-check {\n font-size: 10px;\n}\n\n.filter-btn.tag-btn.all-tags-btn {\n font-weight: 600;\n}\n\n.filter-btn.tag-btn.all-tags-btn i {\n font-size: 11px;\n}\n\n.filter-hint {\n font-weight: 400;\n font-size: 10px;\n color: var(--test-primary);\n text-transform: none;\n letter-spacing: normal;\n}\n\n.tag-filters {\n max-height: 120px;\n overflow-y: auto;\n}\n\n/* KPI Cards */\n.analytics-kpis {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n gap: 16px;\n margin-bottom: 24px;\n}\n\n.kpi-card {\n display: flex;\n align-items: center;\n gap: 16px;\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 20px;\n box-shadow: var(--test-shadow-sm);\n transition: var(--test-transition);\n}\n\n.kpi-card:hover {\n transform: translateY(-2px);\n box-shadow: var(--test-shadow-md);\n}\n\n.kpi-icon {\n width: 48px;\n height: 48px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n border-radius: var(--test-radius-md);\n color: var(--test-primary);\n font-size: 20px;\n}\n\n.kpi-icon.success {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--test-success);\n}\n\n.kpi-icon.info {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.kpi-icon.warning {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--test-warning);\n}\n\n.kpi-content {\n flex: 1;\n min-width: 0;\n}\n\n.kpi-value {\n font-size: 24px;\n font-weight: 700;\n color: var(--test-text);\n line-height: 1.2;\n}\n\n.kpi-label {\n font-size: 12px;\n color: var(--test-text-secondary);\n margin-top: 2px;\n}\n\n.kpi-trend {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 12px;\n font-weight: 600;\n margin-top: 4px;\n padding: 2px 8px;\n border-radius: 10px;\n background: var(--mj-bg-surface-sunken);\n color: var(--test-text-secondary);\n}\n\n.kpi-trend.trend-up {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--test-success);\n}\n\n.kpi-trend.trend-down {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--test-error);\n}\n\n/* Analytics Table */\n.analytics-table-section {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 24px;\n box-shadow: var(--test-shadow-sm);\n}\n\n.analytics-table-section h3 {\n margin: 0 0 16px 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--test-text);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.analytics-table-section h3 i {\n color: var(--test-primary);\n}\n\n.analytics-table-wrapper {\n overflow-x: auto;\n margin: 0 -24px;\n padding: 0 24px;\n}\n\n.analytics-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n}\n\n.analytics-table th {\n text-align: left;\n padding: 12px 16px;\n background: var(--mj-bg-surface-card);\n border-bottom: 2px solid var(--test-border);\n font-weight: 600;\n color: var(--test-text-secondary);\n text-transform: uppercase;\n font-size: 11px;\n letter-spacing: 0.5px;\n white-space: nowrap;\n}\n\n.analytics-table td {\n padding: 14px 16px;\n border-bottom: 1px solid var(--test-border);\n color: var(--test-text);\n}\n\n.analytics-table tbody tr.clickable-row {\n cursor: pointer;\n transition: var(--test-transition);\n}\n\n.analytics-table tbody tr.clickable-row:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n}\n\n.status-chip {\n display: inline-flex;\n align-items: center;\n padding: 4px 10px;\n border-radius: 12px;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n}\n\n.status-chip.status-completed { background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface)); color: var(--test-success); }\n.status-chip.status-passed { background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface)); color: var(--test-success); }\n.status-chip.status-failed { background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface)); color: var(--test-error); }\n.status-chip.status-error { background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface)); color: var(--test-warning); }\n.status-chip.status-running { background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface)); color: var(--test-primary); }\n.status-chip.status-pending { background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface)); color: var(--mj-brand-primary); }\n.status-chip.status-cancelled { background: var(--mj-bg-surface-sunken); color: var(--test-disabled); }\n.status-chip.status-missing { background: var(--mj-bg-surface-sunken); color: var(--test-text-muted); }\n.status-chip.status-timeout { background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface)); color: var(--test-warning); }\n.status-chip.status-skipped { background: var(--mj-bg-surface-sunken); color: var(--test-disabled); }\n\n.pass-rate-cell {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 100px;\n}\n\n.pass-rate-bar {\n height: 6px;\n border-radius: 3px;\n background: var(--test-success);\n transition: width 0.3s ease;\n max-width: 60px;\n}\n\n.pass-rate-bar.medium { background: var(--test-warning); }\n.pass-rate-bar.low { background: var(--test-error); }\n\n/* Analytics Sub-navigation */\n.analytics-subnav {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 8px;\n margin-bottom: 20px;\n box-shadow: var(--test-shadow-sm);\n display: inline-flex;\n}\n\n.subnav-tabs {\n display: flex;\n gap: 4px;\n background: var(--test-bg);\n border-radius: var(--test-radius-md);\n padding: 4px;\n}\n\n.subnav-tab {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 10px 20px;\n border: none;\n border-radius: var(--test-radius-sm);\n background: transparent;\n color: var(--test-text-secondary);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: var(--test-transition);\n}\n\n.subnav-tab:hover {\n color: var(--test-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n}\n\n.subnav-tab.active {\n background: var(--test-surface);\n color: var(--test-primary);\n box-shadow: var(--test-shadow-sm);\n}\n\n.subnav-tab i {\n font-size: 14px;\n}\n\n/* Matrix View */\n.matrix-section {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 16px;\n box-shadow: var(--test-shadow-sm);\n display: flex;\n flex-direction: column;\n max-height: calc(100vh - 280px);\n min-height: 300px;\n}\n\n.matrix-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex-wrap: wrap;\n gap: 12px;\n margin-bottom: 12px;\n padding-bottom: 12px;\n border-bottom: 1px solid var(--test-border);\n flex-shrink: 0;\n}\n\n.matrix-header-right {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.matrix-header h3 {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 0;\n font-size: 15px;\n font-weight: 600;\n color: var(--test-text);\n}\n\n.matrix-header h3 i {\n color: var(--test-primary);\n}\n\n.matrix-run-count {\n font-size: 12px;\n color: var(--test-text-muted);\n font-weight: 500;\n}\n\n/* Matrix filter input */\n.matrix-filter-input {\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--test-border);\n border-radius: 6px;\n padding: 4px 10px;\n min-width: 180px;\n}\n\n.matrix-filter-input i.fa-search {\n color: var(--mj-text-disabled);\n font-size: 12px;\n}\n\n.matrix-filter-input .filter-input {\n border: none;\n background: transparent;\n outline: none;\n font-size: 12px;\n color: var(--test-text);\n width: 100%;\n}\n\n.matrix-filter-input .filter-input::placeholder {\n color: var(--mj-text-disabled);\n}\n\n.clear-filter-btn {\n border: none;\n background: none;\n padding: 2px 4px;\n cursor: pointer;\n color: var(--mj-text-disabled);\n font-size: 10px;\n border-radius: 3px;\n transition: all 0.15s ease;\n}\n\n.clear-filter-btn:hover {\n background: var(--mj-border-default);\n color: var(--mj-text-muted);\n}\n\n/* Scrollable matrix container with fixed height */\n.matrix-scroll-container {\n flex: 1;\n overflow: auto;\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-sm);\n}\n\n.test-matrix {\n display: table;\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n}\n\n.test-matrix thead {\n display: table-header-group;\n}\n\n.test-matrix thead tr {\n display: table-row;\n}\n\n.test-matrix tbody {\n display: table-row-group;\n}\n\n.test-matrix tbody tr {\n display: table-row;\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n\n.test-matrix tbody tr:hover {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n}\n\n.test-matrix tbody tr.row-selected {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 12%, transparent) !important;\n}\n\n.test-matrix tbody tr.row-selected td {\n border-top: 1px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-bottom: 1px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n}\n\n.test-matrix tbody tr.row-selected .seq-cell,\n.test-matrix tbody tr.row-selected .test-name-cell {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 12%, transparent) !important;\n}\n\n.test-matrix tfoot {\n display: table-footer-group;\n}\n\n.test-matrix th,\n.test-matrix td {\n display: table-cell;\n border: 1px solid var(--test-border);\n padding: 8px 12px;\n text-align: center;\n vertical-align: middle;\n}\n\n.test-matrix th {\n background: var(--test-bg);\n font-weight: 600;\n font-size: 11px;\n color: var(--test-text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n position: sticky;\n top: 0;\n z-index: 10;\n border-bottom: 3px solid var(--mj-text-secondary) !important;\n}\n\n/* Sequence column - shared styles */\n.test-matrix .seq-header,\n.test-matrix .seq-cell {\n width: 36px;\n min-width: 36px;\n max-width: 36px;\n text-align: center;\n position: sticky;\n left: 0;\n font-size: 11px;\n color: var(--mj-text-muted);\n border-right: 1px solid var(--test-border);\n padding: 6px 4px !important;\n}\n\n/* Seq header - sticky top AND left, highest z-index */\n.test-matrix .seq-header {\n cursor: pointer;\n font-weight: 600;\n background: var(--test-bg);\n z-index: 12; /* Higher than other headers */\n top: 0;\n}\n\n/* Seq body cells - sticky left only, lower z-index */\n.test-matrix .seq-cell {\n background: var(--test-surface);\n z-index: 2;\n}\n\n.test-matrix .seq-header i {\n font-size: 9px;\n margin-left: 2px;\n opacity: 0.6;\n}\n\n.test-matrix .seq-header:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n.test-matrix .test-name-header {\n text-align: left;\n min-width: 150px;\n max-width: 500px;\n width: auto;\n position: sticky;\n left: 36px;\n background: var(--test-bg);\n z-index: 11;\n border-right: 2px solid var(--test-border);\n cursor: pointer;\n}\n\n.test-matrix .test-name-header i {\n font-size: 9px;\n margin-left: 4px;\n opacity: 0.6;\n}\n\n.test-matrix .test-name-header:hover {\n background: var(--mj-bg-surface-sunken);\n}\n\n.test-matrix .run-header {\n min-width: 120px;\n width: 120px;\n cursor: pointer;\n transition: var(--test-transition);\n}\n\n/* Spacer column absorbs extra width */\n.test-matrix .spacer-header,\n.test-matrix .spacer-cell {\n width: 100%;\n min-width: 20px;\n background: var(--test-bg);\n border: none;\n}\n\n.test-matrix .spacer-cell {\n background: var(--test-surface);\n}\n\n.test-matrix .run-header:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n}\n\n.run-header-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n}\n\n.run-date {\n font-size: 12px;\n font-weight: 600;\n color: var(--test-text);\n}\n\n.run-pass-rate {\n font-size: 11px;\n font-weight: 700;\n padding: 2px 8px;\n border-radius: 10px;\n}\n\n.run-pass-rate.high {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--test-success);\n}\n\n.run-pass-rate.medium {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--test-warning);\n}\n\n.run-pass-rate.low {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--test-error);\n}\n\n.run-tags {\n display: flex;\n gap: 4px;\n}\n\n.tag-tiny {\n font-size: 9px;\n padding: 2px 6px;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--test-primary);\n border-radius: 8px;\n white-space: nowrap;\n max-width: 60px;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Matrix column header tags - emphasized */\n.run-tags-header {\n display: flex;\n flex-wrap: wrap;\n gap: 3px;\n justify-content: center;\n margin-bottom: 4px;\n}\n\n.tag-chip-header {\n display: inline-block;\n padding: 3px 8px;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n color: var(--mj-brand-primary-hover);\n border-radius: 10px;\n font-size: 10px;\n font-weight: 600;\n white-space: nowrap;\n max-width: 70px;\n overflow: hidden;\n text-overflow: ellipsis;\n text-transform: none;\n letter-spacing: normal;\n}\n\n.tag-more-header {\n font-size: 9px;\n color: var(--test-text-secondary);\n padding: 2px 4px;\n}\n\n.test-matrix .test-name-cell {\n text-align: left;\n min-width: 150px;\n max-width: 500px;\n width: auto;\n position: sticky;\n left: 36px;\n background: var(--test-surface);\n z-index: 2;\n border-right: 2px solid var(--test-border);\n padding: 6px 10px !important;\n}\n\n.test-name {\n display: block;\n white-space: nowrap;\n font-weight: 500;\n color: var(--test-text);\n font-size: 12px;\n}\n\n.result-cell {\n position: relative;\n min-width: 120px;\n width: 120px;\n transition: var(--test-transition);\n}\n\n.result-cell.clickable {\n cursor: pointer;\n}\n\n.result-cell.clickable:hover {\n transform: scale(1.1);\n box-shadow: var(--mj-shadow-md);\n z-index: 5;\n}\n\n.result-cell i {\n font-size: 14px;\n}\n\n/* Completed cells have neutral white background - pills tell the story */\n.result-cell.cell-passed,\n.result-cell.cell-failed,\n.result-cell.cell-error,\n.result-cell.cell-timeout,\n.result-cell.cell-running,\n.result-cell.cell-pending {\n background: var(--mj-bg-surface);\n color: var(--test-text);\n}\n\n/* Skipped/not-run cells get hatched background */\n.result-cell.cell-skipped {\n background: repeating-linear-gradient(\n 45deg,\n var(--mj-bg-surface-card),\n var(--mj-bg-surface-card) 4px,\n var(--mj-border-default) 4px,\n var(--mj-border-default) 8px\n );\n color: var(--test-disabled);\n}\n\n.result-cell.cell-none {\n background: var(--mj-bg-surface-card);\n color: var(--test-text-muted);\n}\n\n.cell-score {\n display: block;\n font-size: 10px;\n font-weight: 600;\n margin-top: 2px;\n}\n\n/* Matrix cell evaluation stack - shows multiple eval types horizontally */\n.cell-eval-stack {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n flex-wrap: nowrap;\n}\n\n.result-cell.multi-eval {\n min-width: 120px;\n width: auto;\n}\n\n/* Add timeout status icon */\n.cell-status.status-timeout {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.cell-status {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border-radius: 50%;\n font-size: 11px;\n}\n\n.cell-status.status-passed {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n}\n\n.cell-status.status-failed {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--mj-status-error);\n}\n\n.cell-status.status-error {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.cell-status.status-skipped {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n}\n\n.cell-status.status-running {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary-hover);\n}\n\n.cell-status.status-pending {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.cell-human {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n font-size: 10px;\n}\n\n.cell-auto {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 10px;\n font-weight: 600;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary-hover);\n}\n\n.cell-auto.high {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n}\n\n.cell-auto.medium {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.cell-auto.low {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--mj-status-error);\n}\n\n.cell-none-indicator {\n color: var(--test-text-muted);\n opacity: 0.5;\n}\n\n.matrix-info {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--test-text-muted);\n padding: 12px;\n background: var(--test-bg);\n border-radius: var(--test-radius-sm);\n}\n\n.matrix-info i {\n color: var(--test-primary);\n}\n\n/* ===========================\n Chart View\n =========================== */\n.chart-section {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 24px;\n box-shadow: var(--test-shadow-sm);\n}\n\n.chart-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n flex-wrap: wrap;\n gap: 16px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--test-border);\n}\n\n.chart-header h3 {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--test-text);\n}\n\n.chart-header h3 i {\n color: var(--test-primary);\n}\n\n.chart-legend {\n display: flex;\n gap: 16px;\n flex-wrap: wrap;\n}\n\n.chart-legend .legend-item {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 12px;\n font-weight: 500;\n padding: 4px 10px;\n border-radius: 12px;\n}\n\n.chart-legend .legend-item.chart-passed {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--test-success);\n}\n\n.chart-legend .legend-item.chart-failed {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--test-error);\n}\n\n.chart-legend .legend-item.chart-error {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--test-warning);\n}\n\n.chart-legend .legend-item.chart-skipped {\n background: var(--mj-bg-surface-sunken);\n color: var(--test-disabled);\n}\n\n.chart-container {\n min-height: 500px;\n position: relative;\n overflow: hidden;\n background: var(--mj-bg-surface-card);\n border-radius: var(--test-radius-md);\n border: 1px solid var(--test-border);\n}\n\n.d3-chart {\n width: 100%;\n height: 500px;\n}\n\n.d3-chart svg {\n width: 100%;\n height: 100%;\n}\n\n/* D3 Chart Node Styles */\n.d3-chart .node {\n cursor: pointer;\n transition: transform 0.2s ease;\n}\n\n.d3-chart .node:hover {\n transform: scale(1.05);\n}\n\n.d3-chart .node-label {\n font-size: 11px;\n font-weight: 500;\n fill: var(--test-text);\n pointer-events: none;\n}\n\n.d3-chart .link {\n fill: none;\n stroke-opacity: 0.4;\n transition: stroke-opacity 0.2s ease;\n}\n\n.d3-chart .link:hover {\n stroke-opacity: 0.8;\n}\n\n.d3-chart .tooltip {\n position: absolute;\n padding: 10px 14px;\n background: var(--mj-bg-overlay);\n color: var(--mj-text-inverse);\n border-radius: 8px;\n font-size: 12px;\n pointer-events: none;\n z-index: 100;\n box-shadow: var(--mj-shadow-lg);\n max-width: 250px;\n}\n\n.d3-chart .tooltip-title {\n font-weight: 600;\n margin-bottom: 4px;\n}\n\n.d3-chart .tooltip-value {\n opacity: 0.8;\n}\n\n.chart-info {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--test-text-muted);\n padding: 12px;\n background: var(--test-bg);\n border-radius: var(--test-radius-sm);\n margin-top: 16px;\n}\n\n.chart-info i {\n color: var(--test-primary);\n}\n\n/* ===========================\n Compare Tab\n =========================== */\n.compare-tab {\n animation: fadeIn 0.3s ease-out;\n}\n\n.compare-selection {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n gap: 24px;\n align-items: flex-start;\n margin-bottom: 24px;\n}\n\n.compare-run-selector {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 20px;\n box-shadow: var(--test-shadow-sm);\n}\n\n.compare-run-selector h4 {\n margin: 0 0 16px 0;\n font-size: 14px;\n font-weight: 600;\n color: var(--test-text);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.run-selector-list {\n max-height: 200px;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-bottom: 12px;\n}\n\n.run-selector-item {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 10px 12px;\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-sm);\n cursor: pointer;\n transition: var(--test-transition);\n}\n\n.run-selector-item:hover {\n border-color: var(--test-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n}\n\n.run-selector-item.selected {\n border-color: var(--test-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n}\n\n.run-selector-item.disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.selector-status {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n flex-shrink: 0;\n}\n\n.selector-content {\n flex: 1;\n min-width: 0;\n}\n\n.selector-date {\n font-size: 12px;\n font-weight: 500;\n color: var(--test-text);\n}\n\n.selector-rate {\n font-size: 11px;\n color: var(--test-text-secondary);\n}\n\n.selector-tags {\n display: flex;\n gap: 4px;\n flex-wrap: wrap;\n}\n\n.selected-run-preview {\n padding: 12px;\n background: var(--mj-bg-surface-card);\n border-radius: var(--test-radius-sm);\n border: 1px solid var(--test-border);\n}\n\n.preview-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 8px;\n}\n\n.preview-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n color: var(--test-text-muted);\n}\n\n.clear-btn {\n padding: 4px 8px;\n border: none;\n background: transparent;\n color: var(--test-error);\n font-size: 11px;\n font-weight: 500;\n cursor: pointer;\n}\n\n.clear-btn:hover { text-decoration: underline; }\n\n.preview-details {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n color: var(--test-text);\n}\n\n.preview-rate {\n font-weight: 600;\n color: var(--test-success);\n}\n\n.compare-vs {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 40px;\n height: 40px;\n background: var(--test-bg);\n border-radius: 50%;\n color: var(--test-text-muted);\n font-size: 16px;\n align-self: center;\n margin-top: 60px;\n}\n\n/* Compare Results */\n.compare-results {\n animation: fadeIn 0.3s ease-out;\n}\n\n.compare-summary {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 16px;\n margin-bottom: 24px;\n}\n\n.compare-summary-card {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 20px;\n box-shadow: var(--test-shadow-sm);\n text-align: center;\n}\n\n.compare-summary-card .summary-label {\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n color: var(--test-text-muted);\n margin-bottom: 8px;\n}\n\n.compare-summary-card .summary-value {\n font-size: 24px;\n font-weight: 700;\n color: var(--test-text);\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n}\n\n.compare-summary-card .summary-value.positive { color: var(--test-success); }\n.compare-summary-card .summary-value.negative { color: var(--test-error); }\n\n.compare-summary-card.improved {\n border-left: 4px solid var(--test-success);\n}\n\n.compare-summary-card.improved .summary-value { color: var(--test-success); }\n\n.compare-summary-card.regressed {\n border-left: 4px solid var(--test-error);\n}\n\n.compare-summary-card.regressed .summary-value { color: var(--test-error); }\n\n/* Compare Table */\n.compare-table-section {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 24px;\n box-shadow: var(--test-shadow-sm);\n}\n\n.compare-table-section h3 {\n margin: 0 0 16px 0;\n font-size: 16px;\n font-weight: 600;\n color: var(--test-text);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.compare-table-section h3 i {\n color: var(--test-primary);\n}\n\n.compare-table-wrapper {\n overflow-x: auto;\n}\n\n.compare-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n}\n\n.compare-table th {\n text-align: left;\n padding: 12px 16px;\n background: var(--mj-bg-surface-card);\n border-bottom: 2px solid var(--test-border);\n font-weight: 600;\n color: var(--test-text-secondary);\n text-transform: uppercase;\n font-size: 11px;\n letter-spacing: 0.5px;\n white-space: nowrap;\n}\n\n.compare-table td {\n padding: 14px 16px;\n border-bottom: 1px solid var(--test-border);\n color: var(--test-text);\n}\n\n.compare-table tbody tr.improved {\n background: color-mix(in srgb, var(--mj-status-success) 5%, transparent);\n}\n\n.compare-table tbody tr.regressed {\n background: color-mix(in srgb, var(--mj-status-error) 5%, transparent);\n}\n\n.test-name-cell {\n font-weight: 500;\n max-width: 200px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.positive { color: var(--test-success); }\n.negative { color: var(--test-error); }\n.muted { color: var(--test-text-muted); }\n\n.change-indicator {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 4px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n}\n\n.change-indicator.improved {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--test-success);\n}\n\n.change-indicator.regressed {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--test-error);\n}\n\n.change-indicator.unchanged {\n background: var(--mj-bg-surface-sunken);\n color: var(--test-text-muted);\n}\n\n/* ===========================\n Responsive Analytics/Compare\n =========================== */\n@media (max-width: 1024px) {\n .compare-selection {\n grid-template-columns: 1fr;\n gap: 16px;\n }\n\n .compare-vs {\n margin: 0;\n align-self: center;\n justify-self: center;\n }\n\n .analytics-kpis {\n grid-template-columns: repeat(2, 1fr);\n }\n}\n\n@media (max-width: 768px) {\n .filter-buttons {\n flex-direction: column;\n }\n\n .filter-btn {\n width: 100%;\n text-align: center;\n }\n\n .analytics-kpis {\n grid-template-columns: 1fr;\n }\n\n .kpi-card {\n padding: 16px;\n }\n\n .kpi-value {\n font-size: 20px;\n }\n\n .compare-summary {\n grid-template-columns: repeat(2, 1fr);\n }\n\n .analytics-table-wrapper,\n .compare-table-wrapper {\n margin: 0 -20px;\n padding: 0 20px;\n }\n\n .run-selector-list {\n max-height: 150px;\n }\n}\n\n@media (max-width: 480px) {\n .compare-summary {\n grid-template-columns: 1fr;\n }\n\n .compare-summary-card .summary-value {\n font-size: 20px;\n }\n\n .analytics-table th,\n .analytics-table td,\n .compare-table th,\n .compare-table td {\n padding: 10px 12px;\n font-size: 12px;\n }\n}\n\n/* ===========================\n Matrix Evaluation Indicators\n =========================== */\n\n/* Human Feedback Indicators */\n.cell-human {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 3px;\n min-width: 22px;\n height: 22px;\n border-radius: 50%;\n font-size: 10px;\n}\n\n.cell-human.no-feedback {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-disabled);\n}\n\n.cell-human.no-feedback i {\n font-size: 11px;\n}\n\n.cell-human.has-feedback {\n padding: 0 6px;\n border-radius: 12px;\n min-width: 36px;\n}\n\n.cell-human.has-feedback i {\n font-size: 9px;\n}\n\n.cell-human.has-feedback .rating-value {\n font-weight: 700;\n font-size: 11px;\n}\n\n/* Human rating color coding: red \u22644, yellow 5-6, light-green 7-8, green 9-10 */\n.cell-human.rating-low {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--mj-status-error);\n}\n\n.cell-human.rating-medium {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.cell-human.rating-good {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n}\n\n.cell-human.rating-excellent {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n}\n\n/* Auto Score Indicators */\n.cell-auto {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 3px;\n min-width: 22px;\n height: 22px;\n border-radius: 50%;\n font-size: 10px;\n}\n\n.cell-auto.no-score {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-disabled);\n}\n\n.cell-auto.no-score i {\n font-size: 11px;\n}\n\n.cell-auto.has-score {\n padding: 0 6px;\n border-radius: 12px;\n min-width: 36px;\n}\n\n.cell-auto.has-score i {\n font-size: 9px;\n}\n\n.cell-auto.has-score .score-value {\n font-weight: 700;\n font-size: 10px;\n}\n\n/* Auto score color coding (0-100%): red <50, yellow 50-69, light-green 70-84, green 85+ */\n.cell-auto.score-low {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--mj-status-error);\n}\n\n.cell-auto.score-medium {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.cell-auto.score-good {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n}\n\n.cell-auto.score-excellent {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n}\n\n/* Status indicators in matrix cells */\n.cell-status {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n border-radius: 50%;\n font-size: 11px;\n}\n\n.cell-status.status-passed {\n background: color-mix(in srgb, var(--mj-status-success) 15%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n}\n\n.cell-status.status-failed {\n background: color-mix(in srgb, var(--mj-status-error) 15%, var(--mj-bg-surface));\n color: var(--mj-status-error);\n}\n\n.cell-status.status-error {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.cell-status.status-timeout {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.cell-status.status-skipped,\n.cell-status.status-pending {\n background: var(--mj-bg-surface-sunken);\n color: var(--mj-text-muted);\n}\n\n.cell-status.status-running {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n/* Not-run / Skipped cells with hatch pattern */\n.result-cell.cell-not-run {\n background: repeating-linear-gradient(\n 45deg,\n var(--mj-bg-surface-card),\n var(--mj-bg-surface-card) 4px,\n var(--mj-border-default) 4px,\n var(--mj-border-default) 8px\n );\n color: var(--mj-text-disabled);\n}\n\n.result-cell.cell-not-run .cell-eval-stack {\n opacity: 0.6;\n}\n\n.cell-not-run-indicator {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 22px;\n color: var(--mj-text-disabled);\n font-size: 11px;\n}\n\n/* ===========================\n Matrix Totals Footer Row\n =========================== */\n.test-matrix tfoot {\n position: sticky;\n bottom: 0;\n z-index: 2;\n}\n\n.totals-row {\n background: var(--mj-bg-surface-sunken);\n border-top: 2px solid var(--test-border);\n}\n\n.totals-row td {\n padding: 10px 12px;\n font-weight: 600;\n}\n\n.totals-row .totals-label {\n background: var(--mj-bg-surface-sunken);\n font-size: 12px;\n color: var(--test-text-secondary);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.totals-row .totals-cell {\n background: var(--mj-bg-surface-sunken);\n}\n\n.totals-stack {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n}\n\n.totals-status,\n.totals-human,\n.totals-auto {\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 11px;\n padding: 2px 8px;\n border-radius: 10px;\n white-space: nowrap;\n}\n\n.totals-status {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary);\n}\n\n.totals-status .pass-count {\n font-weight: 700;\n}\n\n.totals-human {\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning);\n}\n\n.totals-human .avg-label {\n font-weight: 700;\n}\n\n.totals-human .count-label {\n font-size: 10px;\n opacity: 0.8;\n}\n\n.totals-auto {\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, var(--mj-bg-surface));\n color: var(--mj-brand-primary-hover);\n}\n\n.totals-auto .avg-label {\n font-weight: 700;\n}\n\n.totals-auto .count-label {\n font-size: 10px;\n opacity: 0.8;\n}\n\n/* ===========================\n Editable Overview Form\n =========================== */\n\n.edit-section {\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n padding: 24px;\n box-shadow: var(--test-shadow-sm);\n}\n\n.edit-section-header {\n margin: 0 0 18px 0;\n}\n\n.edit-section-header h3 {\n margin: 0 0 4px 0;\n font-size: 18px;\n font-weight: 700;\n color: var(--test-text);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.edit-section-header h3 i {\n color: var(--test-primary);\n}\n\n.edit-section-sub {\n margin: 0;\n font-size: 12px;\n color: var(--test-text-muted);\n}\n\n.edit-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 18px;\n}\n\n@media (max-width: 720px) {\n .edit-grid {\n grid-template-columns: 1fr;\n }\n}\n\n.edit-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-width: 0;\n}\n\n.edit-field--full {\n grid-column: 1 / -1;\n}\n\n.edit-field label {\n font-size: 12px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--test-text-muted);\n}\n\n.edit-field .required {\n color: var(--mj-status-error);\n margin-left: 2px;\n}\n\n.config-textarea {\n resize: vertical;\n min-height: 72px;\n font-family: inherit;\n line-height: 1.5;\n}\n\n.select-wrapper {\n position: relative;\n}\n\n.select-wrapper select.config-input {\n padding-right: 36px;\n appearance: none;\n background-color: var(--test-surface);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%2364748b' d='M1.41 0L6 4.59 10.59 0 12 1.41l-6 6-6-6z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 14px center;\n background-size: 10px 7px;\n}\n\n/* Tag editor (chips + inline input) */\n.tag-editor {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 8px 10px;\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-sm);\n background: var(--test-surface);\n transition: var(--test-transition);\n cursor: text;\n min-height: 42px;\n align-items: center;\n}\n\n.tag-editor:focus-within {\n border-color: var(--test-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.tag-chip-editable {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 3px 4px 3px 10px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary-hover);\n border-radius: 999px;\n font-size: 12px;\n font-weight: 600;\n line-height: 1;\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n}\n\n.tag-chip-editable .tag-remove {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 18px;\n height: 18px;\n border-radius: 50%;\n border: none;\n background: transparent;\n color: var(--mj-brand-primary);\n cursor: pointer;\n font-size: 10px;\n transition: background 0.15s;\n}\n\n.tag-chip-editable .tag-remove:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n color: var(--mj-brand-primary-hover);\n}\n\n.tag-input-inline {\n flex: 1;\n min-width: 140px;\n border: none;\n background: transparent;\n outline: none;\n font-size: 13px;\n color: var(--test-text);\n padding: 4px 2px;\n}\n\n.tag-input-inline::placeholder {\n color: var(--test-text-muted);\n}\n\n/* Read-only metadata section */\n.meta-section {\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--test-radius-md);\n padding: 14px 18px;\n border: 1px solid var(--test-border);\n}\n\n.meta-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 14px;\n}\n\n.meta-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.meta-label {\n font-size: 10px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--test-text-muted);\n}\n\n.meta-value {\n font-size: 12px;\n color: var(--test-text-secondary);\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.meta-mono {\n font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;\n font-size: 11px;\n}\n\n/* Dirty indicator next to the title */\n.dirty-indicator {\n display: inline-block;\n margin-left: 8px;\n font-size: 14px;\n color: var(--mj-status-warning);\n line-height: 1;\n animation: pulseDot 1.8s ease-in-out infinite;\n vertical-align: middle;\n}\n\n@keyframes pulseDot {\n 0%, 100% { opacity: 1; transform: scale(1); }\n 50% { opacity: 0.5; transform: scale(0.85); }\n}\n\n.unsaved-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n background: color-mix(in srgb, var(--mj-status-warning) 15%, var(--mj-bg-surface));\n color: var(--mj-status-warning-text, var(--mj-status-warning));\n border: 1px solid color-mix(in srgb, var(--mj-status-warning) 30%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.4px;\n}\n\n.unsaved-pill i {\n font-size: 6px;\n animation: pulseDot 1.8s ease-in-out infinite;\n}\n\n/* Pad the bottom of the tab content when the save bar is visible\n so the bar doesn't cover the last form field. */\n.tab-content.has-savebar {\n padding-bottom: 84px;\n}\n\n/* ===========================\n Sticky Save Bar\n =========================== */\n\n.save-bar {\n position: absolute;\n left: 16px;\n right: 96px; /* leaves room for the bottom-right chat launcher */\n bottom: 16px;\n z-index: 50;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 16px;\n padding: 12px 16px 12px 20px;\n background: var(--mj-bg-surface-elevated, var(--test-surface));\n border: 1px solid color-mix(in srgb, var(--mj-status-warning) 25%, var(--test-border));\n border-radius: 14px;\n box-shadow: 0 12px 32px color-mix(in srgb, var(--mj-status-warning) 10%, rgba(0, 0, 0, 0.18));\n backdrop-filter: blur(8px);\n /* Hidden / off-screen state by default. The .save-bar--visible class\n animates it up into view. Using `transition` (not `animation`)\n guarantees the change runs every time isDirty toggles. */\n opacity: 0;\n transform: translateY(calc(100% + 32px));\n pointer-events: none;\n transition:\n transform 420ms cubic-bezier(0.16, 1, 0.3, 1),\n opacity 260ms cubic-bezier(0.16, 1, 0.3, 1);\n will-change: transform, opacity;\n}\n\n.save-bar--visible {\n opacity: 1;\n transform: translateY(0);\n pointer-events: auto;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .save-bar {\n transition: opacity 180ms ease-out;\n transform: none;\n }\n .save-bar--visible {\n transform: none;\n }\n}\n\n.save-bar-message {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n}\n\n.save-bar-dot {\n color: var(--mj-status-warning);\n font-size: 12px;\n line-height: 1;\n animation: pulseDot 1.8s ease-in-out infinite;\n}\n\n.save-bar-text {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.save-bar-text strong {\n font-size: 14px;\n color: var(--test-text);\n font-weight: 700;\n}\n\n.save-bar-fields {\n font-size: 12px;\n color: var(--test-text-muted);\n}\n\n.save-bar-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n\n.save-bar-btn {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 9px 16px;\n font-size: 13px;\n font-weight: 600;\n border-radius: 8px;\n border: 1px solid transparent;\n cursor: pointer;\n transition: var(--test-transition);\n font-family: inherit;\n}\n\n.save-bar-btn:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.save-bar-btn--primary {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse, white);\n border-color: var(--mj-brand-primary);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary) 35%, transparent);\n}\n\n.save-bar-btn--primary:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover, var(--mj-brand-primary));\n transform: translateY(-1px);\n box-shadow: 0 4px 12px color-mix(in srgb, var(--mj-brand-primary) 45%, transparent);\n}\n\n.save-bar-btn--primary:active:not(:disabled) {\n transform: translateY(0);\n}\n\n.save-bar-btn--ghost {\n background: transparent;\n color: var(--test-text-secondary);\n border-color: var(--test-border);\n}\n\n.save-bar-btn--ghost:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover, var(--mj-bg-surface-card));\n color: var(--test-text);\n border-color: var(--test-text-muted);\n}\n\n.save-bar-kbd {\n display: inline-flex;\n gap: 3px;\n margin-left: 6px;\n opacity: 0.85;\n}\n\n.save-bar-kbd kbd {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 18px;\n height: 18px;\n padding: 0 4px;\n font-size: 10px;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;\n background: color-mix(in srgb, white 18%, transparent);\n border: 1px solid color-mix(in srgb, white 35%, transparent);\n border-radius: 4px;\n color: inherit;\n}\n\n@media (max-width: 640px) {\n .save-bar {\n left: 8px;\n right: 8px;\n bottom: 88px; /* push above the chat launcher on small screens */\n flex-direction: column;\n align-items: stretch;\n padding: 12px;\n }\n .save-bar-actions {\n justify-content: space-between;\n }\n .save-bar-kbd {\n display: none;\n }\n}\n\n/* ===========================\n Tests Tab \u2014 Action Bar + Drag/Remove\n =========================== */\n\n.tests-actionbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 12px 16px;\n margin-bottom: 16px;\n background: var(--test-surface);\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-md);\n}\n\n.tests-actionbar-text {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n min-width: 0;\n}\n\n.tests-actionbar-text strong {\n font-size: 14px;\n color: var(--test-text);\n font-weight: 700;\n}\n\n.tests-actionbar-count {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 22px;\n height: 22px;\n padding: 0 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--mj-bg-surface));\n color: var(--mj-brand-primary-hover);\n font-size: 12px;\n font-weight: 700;\n}\n\n.tests-actionbar-hint {\n font-size: 12px;\n color: var(--test-text-muted);\n}\n\n.tests-actionbar-actions {\n display: flex;\n gap: 8px;\n flex-shrink: 0;\n}\n\n/* Override the default test-item hover translate so drag transforms don't fight it. */\n.tests-list[cdk-drop-list] .test-item:hover,\n.tests-list .test-item.cdk-drag:hover {\n transform: none;\n}\n\n/* The whole row is draggable. .test-content keeps the click-to-open behavior. */\n.test-item.cdk-drag {\n position: relative;\n}\n\n.test-item .test-content {\n cursor: pointer;\n}\n\n/* Drag handle */\n.test-drag-handle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px;\n height: 28px;\n border-radius: 4px;\n color: var(--test-text-muted);\n cursor: grab;\n flex-shrink: 0;\n transition: background 0.15s, color 0.15s;\n user-select: none;\n}\n\n.test-drag-handle:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n color: var(--test-primary);\n}\n\n.test-drag-handle:active,\n.cdk-drag-preview .test-drag-handle {\n cursor: grabbing;\n}\n\n/* Remove (X) button \u2014 hover-revealed */\n.test-remove-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border-radius: 50%;\n border: none;\n background: transparent;\n color: var(--test-text-muted);\n cursor: pointer;\n flex-shrink: 0;\n opacity: 0;\n transition: opacity 0.15s, background 0.15s, color 0.15s;\n}\n\n.test-item:hover .test-remove-btn,\n.test-remove-btn:focus-visible {\n opacity: 1;\n}\n\n.test-remove-btn:hover {\n background: color-mix(in srgb, var(--mj-status-error) 12%, transparent);\n color: var(--mj-status-error);\n}\n\n.test-chevron {\n cursor: pointer;\n}\n\n/* Inline remove-confirm */\n.test-remove-confirm {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 4px 8px 4px 12px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--mj-status-error) 8%, var(--test-surface));\n border: 1px solid color-mix(in srgb, var(--mj-status-error) 30%, transparent);\n flex-shrink: 0;\n}\n\n.test-remove-confirm-text {\n font-size: 12px;\n font-weight: 600;\n color: var(--mj-status-error-text, var(--mj-status-error));\n}\n\n.test-remove-confirm-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 4px 10px;\n border-radius: 6px;\n border: 1px solid transparent;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n font-family: inherit;\n transition: var(--test-transition);\n}\n\n.test-remove-confirm-btn:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.test-remove-confirm-btn--danger {\n background: var(--mj-status-error);\n color: var(--mj-text-inverse, white);\n}\n\n.test-remove-confirm-btn--danger:hover:not(:disabled) {\n background: color-mix(in srgb, var(--mj-status-error) 88%, black);\n}\n\n.test-remove-confirm-btn--ghost {\n background: transparent;\n color: var(--test-text-secondary);\n border-color: var(--test-border);\n}\n\n.test-remove-confirm-btn--ghost:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover, var(--mj-bg-surface-card));\n color: var(--test-text);\n}\n\n/* CDK drag-drop visuals */\n.cdk-drag-preview {\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);\n border-radius: var(--test-radius-md);\n background: var(--test-surface);\n opacity: 0.95;\n}\n\n.cdk-drag-placeholder {\n opacity: 0;\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border: 2px dashed var(--test-primary);\n border-radius: var(--test-radius-md);\n}\n\n.cdk-drag-placeholder * {\n visibility: hidden;\n}\n\n.cdk-drag-animating {\n transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);\n}\n\n.tests-list.cdk-drop-list-dragging .test-item:not(.cdk-drag-placeholder) {\n transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);\n}\n\n/* ===========================\n Add Tests Picker Dialog\n =========================== */\n\n.picker-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n z-index: 200;\n animation: pickerFadeIn 180ms ease-out;\n backdrop-filter: blur(2px);\n}\n\n@keyframes pickerFadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.picker-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: min(720px, calc(100vw - 32px));\n max-height: min(80vh, 720px);\n z-index: 201;\n display: flex;\n flex-direction: column;\n background: var(--test-surface);\n border-radius: var(--test-radius-lg);\n box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);\n animation: pickerSlideUp 220ms cubic-bezier(0.16, 1, 0.3, 1);\n overflow: hidden;\n}\n\n@keyframes pickerSlideUp {\n from {\n opacity: 0;\n transform: translate(-50%, calc(-50% + 16px));\n }\n to {\n opacity: 1;\n transform: translate(-50%, -50%);\n }\n}\n\n.picker-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 18px 20px;\n border-bottom: 1px solid var(--test-border);\n}\n\n.picker-title {\n margin: 0;\n font-size: 16px;\n font-weight: 700;\n color: var(--test-text);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.picker-title i {\n color: var(--test-primary);\n}\n\n.picker-close {\n width: 32px;\n height: 32px;\n border-radius: 8px;\n border: none;\n background: transparent;\n color: var(--test-text-muted);\n cursor: pointer;\n font-size: 14px;\n transition: background 0.15s, color 0.15s;\n}\n\n.picker-close:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover, var(--mj-bg-surface-card));\n color: var(--test-text);\n}\n\n.picker-close:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.picker-search {\n position: relative;\n padding: 14px 20px 0 20px;\n}\n\n.picker-search-icon {\n position: absolute;\n top: 50%;\n left: 32px;\n transform: translateY(-30%);\n color: var(--test-text-muted);\n font-size: 13px;\n pointer-events: none;\n}\n\n.picker-search-input {\n width: 100%;\n padding: 10px 36px 10px 36px;\n border: 1px solid var(--test-border);\n border-radius: var(--test-radius-sm);\n background: var(--test-surface);\n font-size: 14px;\n color: var(--test-text);\n font-family: inherit;\n transition: var(--test-transition);\n}\n\n.picker-search-input:focus {\n outline: none;\n border-color: var(--test-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.picker-search-clear {\n position: absolute;\n top: 50%;\n right: 32px;\n transform: translateY(-30%);\n width: 22px;\n height: 22px;\n border-radius: 50%;\n border: none;\n background: transparent;\n color: var(--test-text-muted);\n cursor: pointer;\n font-size: 11px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.picker-search-clear:hover {\n background: var(--mj-bg-surface-hover, var(--mj-bg-surface-card));\n color: var(--test-text);\n}\n\n.picker-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 20px;\n border-bottom: 1px solid var(--test-border);\n}\n\n.picker-toolbar-status {\n font-size: 12px;\n color: var(--test-text-secondary);\n}\n\n.picker-toolbar-status strong {\n color: var(--mj-brand-primary-hover);\n font-weight: 700;\n}\n\n.picker-toolbar-actions {\n display: flex;\n gap: 14px;\n}\n\n.picker-link-btn {\n border: none;\n background: transparent;\n color: var(--test-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n padding: 0;\n font-family: inherit;\n}\n\n.picker-link-btn:hover:not(:disabled) {\n text-decoration: underline;\n}\n\n.picker-link-btn:disabled {\n color: var(--test-text-muted);\n cursor: not-allowed;\n}\n\n.picker-body {\n flex: 1;\n overflow-y: auto;\n padding: 8px 12px;\n min-height: 200px;\n}\n\n.picker-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 40px;\n}\n\n.picker-list {\n list-style: none;\n padding: 0;\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.picker-row {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 10px 12px;\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.12s, border-color 0.12s;\n border: 1px solid transparent;\n}\n\n.picker-row:hover {\n background: var(--mj-bg-surface-hover, var(--mj-bg-surface-card));\n}\n\n.picker-row:focus-visible {\n outline: none;\n border-color: var(--test-primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.picker-row--selected {\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--test-surface));\n border-color: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n}\n\n.picker-row--selected:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, var(--test-surface));\n}\n\n.picker-check {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n border-radius: 5px;\n border: 1.5px solid var(--test-border);\n background: var(--test-surface);\n color: var(--mj-text-inverse, white);\n font-size: 11px;\n flex-shrink: 0;\n transition: background 0.12s, border-color 0.12s;\n}\n\n.picker-row--selected .picker-check {\n background: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.picker-row-main {\n flex: 1;\n min-width: 0;\n}\n\n.picker-row-name {\n font-size: 13px;\n font-weight: 600;\n color: var(--test-text);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.picker-row-desc {\n font-size: 12px;\n color: var(--test-text-muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n margin-top: 2px;\n}\n\n.picker-row-meta {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n}\n\n.picker-row-pill {\n display: inline-flex;\n align-items: center;\n padding: 2px 8px;\n border-radius: 999px;\n font-size: 10px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.3px;\n background: var(--mj-bg-surface-sunken, var(--test-bg));\n color: var(--test-text-secondary);\n border: 1px solid var(--test-border);\n}\n\n.picker-row-pill--status[data-status=\"active\"] {\n background: color-mix(in srgb, var(--mj-status-success) 12%, var(--mj-bg-surface));\n color: var(--mj-status-success);\n border-color: color-mix(in srgb, var(--mj-status-success) 30%, transparent);\n}\n\n.picker-row-pill--status[data-status=\"disabled\"] {\n background: var(--mj-bg-surface-sunken, var(--test-bg));\n color: var(--test-text-muted);\n}\n\n.picker-row-pill--status[data-status=\"pending\"] {\n background: color-mix(in srgb, var(--mj-status-warning) 12%, var(--mj-bg-surface));\n color: var(--mj-status-warning-text, var(--mj-status-warning));\n border-color: color-mix(in srgb, var(--mj-status-warning) 30%, transparent);\n}\n\n.picker-footer {\n display: flex;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid var(--test-border);\n background: var(--mj-bg-surface-sunken, var(--test-bg));\n}\n\n.picker-btn {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 9px 16px;\n font-size: 13px;\n font-weight: 600;\n border-radius: 8px;\n border: 1px solid transparent;\n cursor: pointer;\n font-family: inherit;\n transition: var(--test-transition);\n}\n\n.picker-btn:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n.picker-btn--primary {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse, white);\n border-color: var(--mj-brand-primary);\n}\n\n.picker-btn--primary:hover:not(:disabled) {\n background: var(--mj-brand-primary-hover, var(--mj-brand-primary));\n}\n\n.picker-btn--ghost {\n background: transparent;\n color: var(--test-text-secondary);\n border-color: var(--test-border);\n}\n\n.picker-btn--ghost:hover:not(:disabled) {\n background: var(--mj-bg-surface-hover, var(--mj-bg-surface-card));\n color: var(--test-text);\n}\n\n@media (max-width: 640px) {\n .picker-dialog {\n width: calc(100vw - 16px);\n max-height: calc(100vh - 32px);\n }\n .tests-actionbar {\n flex-direction: column;\n align-items: stretch;\n }\n .tests-actionbar-actions {\n justify-content: flex-end;\n }\n .test-remove-btn {\n opacity: 1; /* always visible on touch */\n }\n}\n\n"] }]
|
|
4318
|
-
}], null, {
|
|
4990
|
+
}], null, { ChartContainer: [{
|
|
4319
4991
|
type: ViewChild,
|
|
4320
4992
|
args: ['chartContainer']
|
|
4321
4993
|
}], handleKeyboardShortcut: [{
|