| 1 | from pathlib import Path |
| 2 | |
| 3 | |
| 4 | PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| 5 | |
| 6 | |
| 7 | def _read(relative_path: str) -> str: |
| 8 | return (PROJECT_ROOT / relative_path).read_text(encoding="utf-8") |
| 9 | |
| 10 | |
| 11 | def test_welcome_screen_embeds_shared_new_chat_composer() -> None: |
| 12 | welcome = _read("webui/components/welcome/welcome-screen.html") |
| 13 | welcome_store = _read("webui/components/welcome/welcome-store.js") |
| 14 | index = _read("webui/index.html") |
| 15 | discovery_cards = _read( |
| 16 | "plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html" |
| 17 | ) |
| 18 | discovery_store = _read("plugins/_discovery/webui/discovery-store.js") |
| 19 | |
| 20 | assert 'class="welcome-composer"' in welcome |
| 21 | assert 'path="chat/attachments/inputPreview.html"' in welcome |
| 22 | assert 'path="chat/input/chat-bar-input.html"' in welcome |
| 23 | assert "x-text=\"$store.welcomeStore.heroSubtitle\"" in welcome |
| 24 | assert "Hello! I'm Agent Zero" in welcome |
| 25 | assert "is-setup-required" not in welcome |
| 26 | assert "welcome-setup-composer" not in welcome |
| 27 | assert "Configure your models to start chatting" not in welcome |
| 28 | assert "Start Onboarding" not in welcome |
| 29 | assert "openBlockingSetup()" not in welcome |
| 30 | assert '.filter((b) => b.id !== "missing-api-key")' in welcome_store |
| 31 | assert "get heroSubtitle()" in welcome_store |
| 32 | assert 'return "How can I help you today?";' in welcome_store |
| 33 | assert '<h2>Quick Actions</h2>' in welcome |
| 34 | assert 'class="welcome-lower-grid"' in welcome |
| 35 | assert 'x-extension id="welcome-actions-end"' in welcome |
| 36 | assert "grid-template-columns: repeat(4, minmax(0, 1fr));" in welcome |
| 37 | assert '"accounts accounts channels channels"' in welcome |
| 38 | assert '"system system system system"' in welcome |
| 39 | assert "align-items: stretch;" in welcome |
| 40 | assert ".welcome-lower-grid .discovery-slot {\n display: contents;" in welcome |
| 41 | assert ".welcome-lower-grid .discovery-account-card {\n grid-area: accounts;" in welcome |
| 42 | assert "grid-area: system;" in welcome |
| 43 | assert ".welcome-composer .input-row {\n min-height: 4.9rem;" in welcome |
| 44 | assert "border-radius: 8px;" in welcome |
| 45 | assert 'aria-label="Refresh System Resources"' in welcome |
| 46 | assert "refreshBanners(true)" in welcome |
| 47 | assert 'aria-label="Dismiss System Resources"' in welcome |
| 48 | assert "dismissBanner($store.welcomeStore.systemResourceBanner?.id || 'system-resources')" in welcome |
| 49 | assert "welcome-panel-status-icon" not in welcome |
| 50 | assert "monitoring" not in welcome |
| 51 | assert ".welcome-banner {\n --welcome-banner-accent: #2f6bff;" in welcome |
| 52 | assert "grid-template-columns: auto minmax(0, 1fr) auto auto;" in welcome |
| 53 | assert ".welcome-banner.banner-warning {\n --welcome-banner-accent: #f59e0b;" in welcome |
| 54 | assert "border-left: 4px" not in welcome |
| 55 | assert 'class="welcome-banner-cta"' in welcome |
| 56 | assert "executeBannerAction(banner.cta_action)" in welcome |
| 57 | assert "executeBannerAction(action)" in welcome_store |
| 58 | assert "handleBannerHtmlClick($event)" in welcome |
| 59 | assert "handleBannerHtmlClick(event)" in welcome_store |
| 60 | assert "[data-banner-action]" in welcome_store |
| 61 | assert 'action.startsWith("open-modal:")' in welcome_store |
| 62 | assert 'const hashIndex = path.indexOf("#");' in welcome_store |
| 63 | assert 'history.replaceState(null, "", `#${hash}`);' in welcome_store |
| 64 | assert ".welcome-banner-html .btn,\n .welcome-banner-html button" in welcome |
| 65 | assert "background-color: #4248f1;" in welcome |
| 66 | assert 'aria-label="Dismiss Connect Channels"' in discovery_cards |
| 67 | assert "dismissFeatureCards()" in discovery_cards |
| 68 | assert "dismissFeatureCards() {" in discovery_store |
| 69 | assert 'class="discovery-feature-card"\n type="button"' in discovery_cards |
| 70 | assert "`${card.cta_text || 'Connect'} ${card.title}`" in discovery_cards |
| 71 | assert "discovery-feature-card > .btn" not in discovery_cards |
| 72 | assert "discovery-feature-head" not in discovery_cards |
| 73 | assert "discovery-account-header" in discovery_cards |
| 74 | assert "discovery-account-cta" in discovery_cards |
| 75 | assert "discovery-account-icon" not in discovery_cards |
| 76 | assert "some(b => b.id === 'missing-api-key')" not in discovery_cards |
| 77 | |
| 78 | assert "x-if=\"$store.welcomeStore && $store.welcomeStore.isVisible\"" in index |
| 79 | assert "x-if=\"!$store.welcomeStore || !$store.welcomeStore.isVisible\"" in index |
| 80 | assert "Connect Channels" in discovery_cards |
| 81 | assert "oauthAccountCards" in discovery_cards |
| 82 | assert "discovery-account-card" in discovery_cards |
| 83 | assert "topHeroCards" not in discovery_cards |
| 84 | assert "bottomHeroCards" not in discovery_cards |
| 85 | assert "background: var(--color-background);" in welcome |
| 86 | assert "radial-gradient" not in welcome |
| 87 | |
| 88 | |
| 89 | def test_welcome_composer_can_create_a_chat_before_sending() -> None: |
| 90 | input_store = _read("webui/components/chat/input/input-store.js") |
| 91 | chats_store = _read("webui/components/sidebar/chats/chats-store.js") |
| 92 | index_js = _read("webui/index.js") |
| 93 | gate_store = _read("webui/components/chat/model-gate-store.js") |
| 94 | gate_component = _read("webui/components/chat/model-setup-gate.html") |
| 95 | |
| 96 | assert 'return "Ask anything to start a new chat";' in input_store |
| 97 | assert "if (!chatsStore.selected" in input_store |
| 98 | assert "await chatsStore.newChat()" in input_store |
| 99 | optimistic_user = 'await setMessages([{ id: messageId, type: "user"' |
| 100 | assert index_js.count(optimistic_user) == 1 |
| 101 | assert index_js.index(optimistic_user) < index_js.index("if (hasAttachments)") |
| 102 | assert "return response.ctxid;" in chats_store |
| 103 | assert 'return "arrow_forward";' in input_store |
| 104 | assert "modelGateStore.canSendToModel()" in index_js |
| 105 | assert "modelGateStore.mergeSyntheticMessages(snapshot.logs, context)" in index_js |
| 106 | assert 'type: "model_setup_gate"' in gate_store |
| 107 | assert 'STORAGE_KEY = "a0:model-gate-pending:v1"' in gate_store |
| 108 | assert "SYNTHETIC_MESSAGE_NO_BASE = Number.MAX_SAFE_INTEGER - 2" in gate_store |
| 109 | assert "return synthetic.length ? [...(logs || []), ...synthetic] : logs;" in gate_store |
| 110 | assert "restorePending()" in gate_store |
| 111 | assert "savePending()" in gate_store |
| 112 | assert "clearSavedPending()" in gate_store |
| 113 | assert "sessionStorage.setItem(STORAGE_KEY" in gate_store |
| 114 | assert "sessionStorage.getItem(STORAGE_KEY)" in gate_store |
| 115 | assert "sessionStorage.removeItem(STORAGE_KEY)" in gate_store |
| 116 | assert 'import("/plugins/_oauth/webui/oauth-config-store.js")' in gate_store |
| 117 | assert "refreshConnectedAccountState()" in gate_store |
| 118 | assert "accountConnected" in gate_store |
| 119 | assert "accountLabel" in gate_store |
| 120 | assert "oauthConfigStore.connectedProviderCards()[0] || null" in gate_store |
| 121 | assert "autoApplyConnectedProviderIfNeeded" not in gate_store |
| 122 | assert "void this.dispatchPendingIfConfigured();" in gate_store |
| 123 | assert "this.choice = \"\";" in gate_store |
| 124 | assert 'document.addEventListener("model-configured"' in gate_store |
| 125 | assert 'document.addEventListener("model-setup-changed"' in gate_store |
| 126 | assert "bypassModelGate: true" in gate_store |
| 127 | assert "modelConfigStore.openPresetEditor(" in gate_store |
| 128 | assert 'openPluginConfig("_oauth"' not in gate_store |
| 129 | assert "Your message sends automatically once a model is connected." in gate_component |
| 130 | assert "openOnboarding('cloud')" in gate_component |
| 131 | assert "openOnboarding('account')" in gate_component |
| 132 | assert "openOnboarding('local')" in gate_component |
| 133 | assert gate_component.index("openOnboarding('cloud')") < gate_component.index("openOnboarding('account')") < gate_component.index("openOnboarding('local')") |
| 134 | assert "openOauthConfiguration" not in gate_component |
| 135 | assert "Show accounts" not in gate_component |
| 136 | assert "model-gate-accounts" not in gate_component |
| 137 | assert "Choose models" in gate_component |
| 138 | assert "Advanced model configuration" in gate_component |
| 139 | assert "Advanced settings" not in gate_component |
| 140 | assert "model-gate-fields" not in gate_component |
| 141 | assert "Connect model" not in gate_component |
| 142 | assert "accountsOpen" not in gate_store |
| 143 | assert "saveInlineSetup" not in gate_store |
| 144 | assert """.model-gate-card { |
| 145 | display: grid; |
| 146 | gap: 0.75rem; |
| 147 | }""" in gate_component |
| 148 | # The composer never hard-blocks sending: the in-chat gate guides users instead. |
| 149 | assert "Connect a model to send" not in input_store |
| 150 | assert "blocked" not in input_store |
| 151 | assert "sendDisabled" not in input_store |
| 152 | assert "isBlockingSend" not in gate_store |
| 153 | assert "isBlockingSend" not in index_js |
| 154 | |
| 155 | |
| 156 | def test_welcome_composer_does_not_overlap_idle_progress_placeholder() -> None: |
| 157 | input_store = _read("webui/components/chat/input/input-store.js") |
| 158 | |
| 159 | assert "!!chatsStore.selected &&\n state !== \"all\"" in input_store |
| 160 | assert '!(state === "stop" && messageQueueStore?.hasQueue)' in input_store |
| 161 | |
| 162 | |
| 163 | def test_welcome_composer_buttons_keep_target_geometry_without_glow() -> None: |
| 164 | chat_bar = _read("webui/components/chat/input/chat-bar-input.html") |
| 165 | mic_css = _read("plugins/_whisper_stt/webui/whisper-stt.css") |
| 166 | |
| 167 | assert "#send-button {\n order: 2;\n border-radius: 12px;" in chat_bar |
| 168 | assert "background-color: #4248f1;" in chat_bar |
| 169 | assert "background-color: #e67e22;" in chat_bar |
| 170 | assert "linear-gradient" not in chat_bar |
| 171 | assert "#microphone-button {\n order: 1;" in mic_css |
| 172 | assert "border-radius: 12px;" in mic_css |
| 173 | assert "background-color: transparent;" in mic_css |
| 174 | assert "#microphone-button.mic-inactive {\n color: grey;" in mic_css |
| 175 | assert "background-color: grey;" not in mic_css |
| 176 | assert "#microphone-button.mic-listening {\n color: red;" in mic_css |
| 177 | assert "#microphone-button.mic-recording {\n color: green;" in mic_css |
| 178 | assert "#microphone-button.mic-activating svg" in mic_css |
| 179 | assert "rgba(34, 68, 255" not in chat_bar |
| 180 | assert "rgba(217, 119, 6" not in chat_bar |
| 181 | |
| 182 | |
| 183 | def test_welcome_hover_and_focus_borders_use_neutral_tokens() -> None: |
| 184 | surfaces = "\n".join( |
| 185 | [ |
| 186 | _read("webui/components/welcome/welcome-screen.html"), |
| 187 | _read("webui/components/chat/input/chat-bar-input.html"), |
| 188 | _read("plugins/_discovery/extensions/webui/welcome-actions-end/discovery-cards.html"), |
| 189 | ] |
| 190 | ) |
| 191 | |
| 192 | assert "#315cf6" not in surfaces |
| 193 | assert "rgba(34, 68, 255" not in surfaces |
| 194 | assert "linear-gradient" not in surfaces |
| 195 | assert "border-color: color-mix(in srgb, var(--color-primary" not in surfaces |
| 196 | assert "border-color: var(--color-primary" not in surfaces |
| 197 | assert "border-color: color-mix(in srgb, var(--color-border) 88%, var(--color-text) 12%)" in surfaces |
| 198 | |
| 199 | |
| 200 | def test_composer_creates_visual_code_blocks_from_entered_fence() -> None: |
| 201 | chat_bar = _read("webui/components/chat/input/chat-bar-input.html") |
| 202 | input_store = _read("webui/components/chat/input/input-store.js") |
| 203 | |
| 204 | assert 'contenteditable="true"' in chat_bar |
| 205 | assert "font-family: var(--font-family-main" in chat_bar |
| 206 | assert ".composer-code-content" in chat_bar |
| 207 | assert "font-family: var(--font-family-code" in chat_bar |
| 208 | assert "FENCE_LINE_RE" in input_store |
| 209 | assert "_tryCreateCodeBlock($event)" in input_store |
| 210 | assert 'data-code-block' in input_store |
| 211 | assert "handlePaste($event)" in input_store |
| 212 | assert "_insertPlainText(text)" in input_store |
| 213 | assert "is-code-context" not in chat_bar |
| 214 | assert "_isCaretInsideFence" not in input_store |