diff --git a/src/addons/addons.js b/src/addons/addons.js index c58158695..7c8148bc2 100644 --- a/src/addons/addons.js +++ b/src/addons/addons.js @@ -81,12 +81,14 @@ const addons = [ 'tw-disable-cloud-variables', 'tw-disable-compiler', 'editor-stepping', - 'qcode' + 'qcode', + 'boxy-assistant' ]; const newAddons = [ 'expanded-backpack', - 'qcode' + 'qcode', + 'boxy-assistant' ]; // eslint-disable-next-line import/no-commonjs diff --git a/src/addons/addons/boxy-assistant/_manifest_entry.js b/src/addons/addons/boxy-assistant/_manifest_entry.js new file mode 100644 index 000000000..b1c6d7bb2 --- /dev/null +++ b/src/addons/addons/boxy-assistant/_manifest_entry.js @@ -0,0 +1,33 @@ +/* OmniBlocks custom addon */ +const manifest = { + "editorOnly": true, + "noTranslations": true, + "name": "Boxy AI Assistant", + "description": "An AI-powered assistant in the form of Boxy to help you learn and code better. Features local AI models and helpful animations.", + "credits": [ + { + "name": "supervoidcoder", + "link": "https://github.com/supervoidcoder" + }, + { + "name": "OmniBlocks Team" + } + ], + "dynamicDisable": true, + "userscripts": [ + { + "url": "userscript.js" + } + ], + "userstyles": [ + { + "url": "style.css" + } + ], + "tags": [ + "featured", + "new" + ], + "enabledByDefault": false +}; +export default manifest; diff --git a/src/addons/addons/boxy-assistant/_runtime_entry.js b/src/addons/addons/boxy-assistant/_runtime_entry.js new file mode 100644 index 000000000..f8ac5750a --- /dev/null +++ b/src/addons/addons/boxy-assistant/_runtime_entry.js @@ -0,0 +1,7 @@ +/* OmniBlocks custom addon */ +import _js from "./userscript.js"; +import _css from "!css-loader!./style.css"; +export const resources = { + "userscript.js": _js, + "style.css": _css, +}; diff --git a/src/addons/addons/boxy-assistant/style.css b/src/addons/addons/boxy-assistant/style.css new file mode 100644 index 000000000..c5c1db7f0 --- /dev/null +++ b/src/addons/addons/boxy-assistant/style.css @@ -0,0 +1,355 @@ +/* Boxy AI Assistant Styles */ + +/* Container that overlays the entire editor */ +.boxy-assistant-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9999; + overflow: hidden; +} + +/* Boxy character */ +.boxy-character { + position: absolute; + width: 150px; + height: 156px; + pointer-events: auto; + cursor: pointer; + transform-origin: center center; + user-select: none; +} + +.boxy-character.dragging { + cursor: grabbing; +} + +.boxy-character:hover { + filter: drop-shadow(0 0 10px rgba(0, 186, 135, 0.5)); +} + +.boxy-character:focus { + outline: 3px solid #00ba87; + outline-offset: 5px; +} + +.boxy-svg-wrapper { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.boxy-svg-wrapper svg { + width: 100%; + height: auto; +} + +/* Text bubble */ +.boxy-text-bubble { + position: fixed; + max-width: 300px; + min-width: 200px; + background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%); + border-radius: 20px; + padding: 15px 20px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + pointer-events: auto; + animation: bubbleAppear 0.3s ease; + z-index: 10000; +} + +@keyframes bubbleAppear { + from { + opacity: 0; + transform: scale(0.8) translateY(10px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.boxy-text-bubble::after { + content: ""; + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-top: 15px solid #00ba87; +} + +.boxy-bubble-content { + color: white; + font-family: "Helvetica Neue", Arial, sans-serif; + font-size: 14px; + line-height: 1.5; +} + +.boxy-message { + margin: 0; + font-weight: 500; +} + +/* Chat Interface */ +.boxy-chat-interface { + position: fixed; + width: 400px; + max-width: calc(100vw - 40px); + height: 500px; + max-height: calc(100vh - 40px); + background: white; + border-radius: 15px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); + pointer-events: auto; + z-index: 10001; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.boxy-chat-header { + background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%); + color: white; + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.boxy-chat-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.boxy-chat-close { + background: none; + border: none; + color: white; + font-size: 28px; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: background 0.2s; +} + +.boxy-chat-close:hover { + background: rgba(255, 255, 255, 0.2); +} + +.boxy-chat-messages { + flex: 1; + overflow-y: auto; + padding: 15px; + background: #f5f5f5; +} + +.boxy-chat-message { + display: flex; + gap: 10px; + margin-bottom: 15px; + align-items: flex-start; +} + +.boxy-message-bot { + flex-direction: row; +} + +.boxy-message-user { + flex-direction: row-reverse; +} + +.boxy-chat-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} + +.boxy-message-user .boxy-chat-avatar { + background: #888; +} + +.boxy-chat-text { + background: white; + padding: 10px 15px; + border-radius: 15px; + max-width: 70%; + word-wrap: break-word; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); +} + +.boxy-message-user .boxy-chat-text { + background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%); + color: white; +} + +.boxy-thinking .boxy-chat-text { + opacity: 0.7; + font-style: italic; +} + +.boxy-chat-input-area { + padding: 15px; + background: white; + border-top: 1px solid #ddd; + display: flex; + gap: 10px; +} + +.boxy-chat-input { + flex: 1; + padding: 10px 15px; + border: 2px solid #ddd; + border-radius: 25px; + font-size: 14px; + outline: none; + transition: border-color 0.2s; +} + +.boxy-chat-input:focus { + border-color: #00ba87; +} + +.boxy-chat-send { + padding: 10px 20px; + background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%); + color: white; + border: none; + border-radius: 25px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.boxy-chat-send:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 186, 135, 0.4); +} + +.boxy-chat-send:active { + transform: translateY(0); +} + +.boxy-api-key-setup { + padding: 15px; + background: #fff8e1; + border-top: 1px solid #ffe082; +} + +.boxy-api-key-setup p { + margin: 0 0 10px 0; + font-size: 13px; +} + +.boxy-api-key-setup strong { + color: #f57c00; +} + +.boxy-api-key-setup a { + color: #0067bb; + text-decoration: none; + font-weight: 600; +} + +.boxy-api-key-setup a:hover { + text-decoration: underline; +} + +.boxy-api-key-input { + width: 100%; + padding: 10px; + border: 2px solid #ffe082; + border-radius: 8px; + font-size: 14px; + margin-bottom: 10px; + box-sizing: border-box; +} + +.boxy-api-key-save { + width: 100%; + padding: 10px; + background: linear-gradient(135deg, #0067bb 0%, #00ba87 100%); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s; +} + +.boxy-api-key-save:hover { + transform: translateY(-2px); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .boxy-character { + width: 100px; + height: 104px; + } + + .boxy-text-bubble { + max-width: 200px; + font-size: 12px; + padding: 10px 15px; + } + + .boxy-chat-interface { + width: calc(100vw - 20px); + height: calc(100vh - 20px); + left: 10px !important; + top: 10px !important; + } +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .boxy-text-bubble { + box-shadow: 0 4px 20px rgba(255, 255, 255, 0.1); + } + + .boxy-chat-interface { + background: #2a2a2a; + color: #fff; + } + + .boxy-chat-messages { + background: #1a1a1a; + } + + .boxy-chat-text { + background: #3a3a3a; + color: #fff; + } + + .boxy-chat-input-area { + background: #2a2a2a; + border-top-color: #444; + } + + .boxy-chat-input { + background: #3a3a3a; + color: #fff; + border-color: #555; + } +} diff --git a/src/addons/addons/boxy-assistant/userscript.js b/src/addons/addons/boxy-assistant/userscript.js new file mode 100644 index 000000000..d8eb7f84d --- /dev/null +++ b/src/addons/addons/boxy-assistant/userscript.js @@ -0,0 +1,479 @@ +/* Boxy AI Assistant - Main Userscript */ + +export default async function ({ addon, console }) { + console.log("Boxy AI Assistant initializing..."); + + // Create the Boxy container that overlays the entire editor + const boxyContainer = document.createElement("div"); + boxyContainer.className = "boxy-assistant-container"; + boxyContainer.setAttribute("role", "complementary"); + boxyContainer.setAttribute("aria-label", "Boxy AI Assistant - Your coding helper"); + addon.tab.displayNoneWhileDisabled(boxyContainer, { display: "block" }); + + // Create the Boxy character element + const boxyCharacter = document.createElement("div"); + boxyCharacter.className = "boxy-character"; + boxyCharacter.setAttribute("role", "button"); + boxyCharacter.setAttribute("aria-label", "Boxy - Click to chat, or drag to move"); + boxyCharacter.setAttribute("tabindex", "0"); + boxyCharacter.innerHTML = ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ `; + + // Create text bubble for Boxy's messages + const textBubble = document.createElement("div"); + textBubble.className = "boxy-text-bubble"; + textBubble.style.display = "none"; + textBubble.setAttribute("role", "status"); + textBubble.setAttribute("aria-live", "polite"); + textBubble.innerHTML = ` +
+

Hi! I'm Boxy, your AI assistant!

+
+ `; + + // Create chat interface + const chatInterface = document.createElement("div"); + chatInterface.className = "boxy-chat-interface"; + chatInterface.style.display = "none"; + chatInterface.setAttribute("role", "dialog"); + chatInterface.setAttribute("aria-label", "Chat with Boxy"); + chatInterface.innerHTML = ` +
+

Chat with Boxy

+ +
+
+
+
🤖
+
Hi! I'm Boxy, your AI assistant! I can help you learn to code. To get started, you'll need to provide an API key from Pollinations AI.
+
+
+
+ + +
+
+

⚠️ API Key Required

+

Boxy uses Pollinations AI (BYOK - Bring Your Own Key). Get your free key at enter.pollinations.ai

+ + +
+ `; + + // Assemble the components + boxyContainer.appendChild(boxyCharacter); + boxyContainer.appendChild(textBubble); + boxyContainer.appendChild(chatInterface); + + // State variables + let xOffset = 0; + let yOffset = 0; + let boxyHideTimeoutId = null; + let apiKey = localStorage.getItem('boxy-api-key') || ''; + let chatHistory = []; + + // Wait for the editor to be ready + while (true) { + await addon.tab.waitForElement('[class*="stage-wrapper"]', { + markAsSeen: true, + reduxEvents: ["scratch-gui/mode/SET_PLAYER", "fontsLoaded/SET_FONTS_LOADED"], + }); + + if (addon.tab.editorMode === "editor") { + document.body.appendChild(boxyContainer); + console.log("Boxy added to editor!"); + + initializeBoxyPosition(); + makeBoxyDraggable(); + setupChatInterface(); + + setTimeout(() => { + showBoxyMessage("Hi! I'm Boxy! Click me to chat, or drag me to move!"); + }, 1000); + + break; + } + } + + // Initialize Boxy's starting position + function initializeBoxyPosition() { + boxyCharacter.style.left = "calc(100vw - 200px)"; + boxyCharacter.style.top = "calc(100vh - 300px)"; + } + + // Update text bubble position relative to Boxy + function updateBubblePosition() { + const boxyRect = boxyCharacter.getBoundingClientRect(); + textBubble.style.left = `${boxyRect.left}px`; + textBubble.style.top = `${boxyRect.top - textBubble.offsetHeight - 20}px`; + } + + // Make Boxy draggable + function makeBoxyDraggable() { + let isDragging = false; + let dragMoved = false; + let currentX = 0; + let currentY = 0; + let initialX = 0; + let initialY = 0; + + function dragStart(e) { + if (e.type === "mousedown") { + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + } else if (e.type === "touchstart") { + initialX = e.touches[0].clientX - xOffset; + initialY = e.touches[0].clientY - yOffset; + } + + if (e.target === boxyCharacter || boxyCharacter.contains(e.target)) { + isDragging = true; + dragMoved = false; + boxyCharacter.classList.add("dragging"); + + // Attach drag listeners only when dragging starts + document.addEventListener("mousemove", drag); + document.addEventListener("mouseup", dragEnd); + document.addEventListener("touchmove", drag); + document.addEventListener("touchend", dragEnd); + } + } + + function drag(e) { + if (isDragging) { + e.preventDefault(); + dragMoved = true; + + if (e.type === "mousemove") { + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + } else if (e.type === "touchmove") { + currentX = e.touches[0].clientX - initialX; + currentY = e.touches[0].clientY - initialY; + } + + xOffset = currentX; + yOffset = currentY; + + setBoxyTranslate(currentX, currentY); + updateBubblePosition(); + } + } + + function dragEnd(e) { + if (isDragging) { + // Only update if we actually computed positions + if (typeof currentX === "number" && typeof currentY === "number") { + initialX = currentX; + initialY = currentY; + } + + isDragging = false; + boxyCharacter.classList.remove("dragging"); + + // Remove drag listeners + document.removeEventListener("mousemove", drag); + document.removeEventListener("mouseup", dragEnd); + document.removeEventListener("touchmove", drag); + document.removeEventListener("touchend", dragEnd); + + // If we didn't move much, treat it as a click + if (!dragMoved) { + toggleChat(); + } + } + } + + function setBoxyTranslate(xPos, yPos) { + boxyCharacter.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`; + } + + // Keyboard support for accessibility + function handleKeyDown(e) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + toggleChat(); + } else if (e.key.startsWith("Arrow")) { + e.preventDefault(); + const step = e.shiftKey ? 20 : 5; + switch (e.key) { + case "ArrowLeft": + xOffset -= step; + break; + case "ArrowRight": + xOffset += step; + break; + case "ArrowUp": + yOffset -= step; + break; + case "ArrowDown": + yOffset += step; + break; + } + currentX = xOffset; + currentY = yOffset; + setBoxyTranslate(xOffset, yOffset); + updateBubblePosition(); + } + } + + boxyCharacter.addEventListener("mousedown", dragStart); + boxyCharacter.addEventListener("touchstart", dragStart); + boxyCharacter.addEventListener("keydown", handleKeyDown); + + // Cleanup function + const cleanup = () => { + boxyCharacter.removeEventListener("mousedown", dragStart); + boxyCharacter.removeEventListener("touchstart", dragStart); + boxyCharacter.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("mousemove", drag); + document.removeEventListener("mouseup", dragEnd); + document.removeEventListener("touchmove", drag); + document.removeEventListener("touchend", dragEnd); + }; + + return cleanup; + } + + // Show a message in Boxy's text bubble + function showBoxyMessage(message, duration = 5000) { + const messageElement = textBubble.querySelector(".boxy-message"); + messageElement.textContent = message; + textBubble.style.display = "block"; + updateBubblePosition(); + + // Clear any existing timeout + if (boxyHideTimeoutId !== null) { + clearTimeout(boxyHideTimeoutId); + boxyHideTimeoutId = null; + } + + // Auto-hide after duration + if (duration > 0) { + boxyHideTimeoutId = setTimeout(() => { + textBubble.style.display = "none"; + boxyHideTimeoutId = null; + }, duration); + } + } + + // Toggle chat interface + function toggleChat() { + const isVisible = chatInterface.style.display !== "none"; + chatInterface.style.display = isVisible ? "none" : "block"; + + if (!isVisible) { + // Position chat near Boxy + const boxyRect = boxyCharacter.getBoundingClientRect(); + chatInterface.style.left = `${Math.max(20, boxyRect.left - 320)}px`; + chatInterface.style.top = `${Math.max(20, boxyRect.top)}px`; + + // Focus input + const input = chatInterface.querySelector(".boxy-chat-input"); + if (apiKey) { + input.focus(); + } + } + } + + // Setup chat interface handlers + function setupChatInterface() { + const closeBtn = chatInterface.querySelector(".boxy-chat-close"); + const sendBtn = chatInterface.querySelector(".boxy-chat-send"); + const input = chatInterface.querySelector(".boxy-chat-input"); + const apiKeyInput = chatInterface.querySelector(".boxy-api-key-input"); + const apiKeySaveBtn = chatInterface.querySelector(".boxy-api-key-save"); + const messagesContainer = chatInterface.querySelector(".boxy-chat-messages"); + const apiKeySetup = chatInterface.querySelector(".boxy-api-key-setup"); + + // Load saved API key + if (apiKey) { + apiKeySetup.style.display = "none"; + apiKeyInput.value = apiKey; + } + + closeBtn.addEventListener("click", () => { + chatInterface.style.display = "none"; + }); + + apiKeySaveBtn.addEventListener("click", () => { + const key = apiKeyInput.value.trim(); + if (key) { + apiKey = key; + localStorage.setItem('boxy-api-key', key); + apiKeySetup.style.display = "none"; + addChatMessage("API key saved! You can now chat with me.", "bot"); + input.focus(); + } else { + alert("Please enter a valid API key"); + } + }); + + async function sendMessage() { + const message = input.value.trim(); + if (!message) return; + + if (!apiKey) { + addChatMessage("Please set up your API key first!", "bot"); + return; + } + + // Add user message + addChatMessage(message, "user"); + input.value = ""; + + // Show thinking state + addChatMessage("...", "bot", true); + + try { + // Build system prompt with Boxy's personality and guidelines + const systemPrompt = `You are Boxy, a friendly AI assistant designed to help kids learn coding in OmniBlocks (a Scratch-based platform). + +Your role: +- Help users understand coding concepts +- Suggest approaches and brainstorm ideas +- Guide users through problem-solving +- Explain how blocks and features work +- Encourage learning and experimentation + +Important rules: +- NEVER write complete code for users - they must learn by doing +- If asked to "write code" or "make a program", politely refuse and offer to teach them how instead +- Use simple, kid-friendly language +- Be encouraging and supportive +- Keep responses concise and focused + +Your catchphrase when refusing to write code: "Sorry, buddy. I can suggest things to you, help you learn a concept, or brainstorm fun things, but if you're here to vibe code, this ain't the place for you, pal." + +User's question: ${message}`; + + // Call Pollinations AI API with system prompt + const response = await fetch(`https://text.pollinations.ai/prompt/${encodeURIComponent(systemPrompt)}`, { + headers: { + 'Authorization': `Bearer ${apiKey}` + } + }); + + // Remove thinking message + const thinkingMsg = messagesContainer.querySelector(".boxy-thinking"); + if (thinkingMsg) thinkingMsg.remove(); + + if (!response.ok) { + throw new Error(`API Error: ${response.status}`); + } + + const aiResponse = await response.text(); + addChatMessage(aiResponse, "bot"); + + // Add to history + chatHistory.push({ role: "user", content: message }); + chatHistory.push({ role: "assistant", content: aiResponse }); + + } catch (error) { + console.error("Chat error:", error); + const thinkingMsg = messagesContainer.querySelector(".boxy-thinking"); + if (thinkingMsg) thinkingMsg.remove(); + addChatMessage(`Oops! Something went wrong: ${error.message}. Please check your API key.`, "bot"); + } + } + + function addChatMessage(text, sender, isThinking = false) { + const msgDiv = document.createElement("div"); + msgDiv.className = `boxy-chat-message boxy-message-${sender}`; + if (isThinking) msgDiv.classList.add("boxy-thinking"); + + msgDiv.innerHTML = ` +
${sender === "bot" ? "🤖" : "👤"}
+
${text}
+ `; + + messagesContainer.appendChild(msgDiv); + messagesContainer.scrollTop = messagesContainer.scrollHeight; + } + + sendBtn.addEventListener("click", sendMessage); + input.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + sendMessage(); + } + }); + } + + // Expose API for future integrations + window.boxyAPI = { + showMessage: showBoxyMessage, + moveTo: (x, y) => { + // Reset offsets when programmatically moving + xOffset = x; + yOffset = y; + boxyCharacter.style.transform = `translate3d(${x}px, ${y}px, 0)`; + updateBubblePosition(); + }, + openChat: () => { + if (chatInterface.style.display === "none") { + toggleChat(); + } + }, + closeChat: () => { + chatInterface.style.display = "none"; + } + }; + + // Cleanup on addon disable + addon.self.addEventListener("disabled", () => { + if (boxyHideTimeoutId !== null) { + clearTimeout(boxyHideTimeoutId); + } + if (window.boxyAPI) { + delete window.boxyAPI; + } + }); + + console.log("Boxy AI Assistant fully initialized!"); +} diff --git a/src/addons/generated/addon-entries.js b/src/addons/generated/addon-entries.js index 2a369010f..9d0bfad51 100644 --- a/src/addons/generated/addon-entries.js +++ b/src/addons/generated/addon-entries.js @@ -81,4 +81,5 @@ export default { "tw-disable-cloud-variables": () => import(/* webpackChunkName: "addon-entry-tw-disable-cloud-variables" */ "../addons/tw-disable-cloud-variables/_runtime_entry.js"), "tw-disable-compiler": () => import(/* webpackChunkName: "addon-entry-tw-disable-compiler" */ "../addons/tw-disable-compiler/_runtime_entry.js"), "editor-stepping": () => import(/* webpackChunkName: "addon-entry-editor-stepping" */ "../addons/editor-stepping/_runtime_entry.js"), + "boxy-assistant": () => import(/* webpackChunkName: "addon-entry-boxy-assistant" */ "../addons/boxy-assistant/_runtime_entry.js"), }; diff --git a/src/addons/generated/addon-manifests.js b/src/addons/generated/addon-manifests.js index 9f0a4ea1a..5a578b40c 100644 --- a/src/addons/generated/addon-manifests.js +++ b/src/addons/generated/addon-manifests.js @@ -79,6 +79,7 @@ import _tw_remove_feedback from "../addons/tw-remove-feedback/_manifest_entry.js import _tw_disable_cloud_variables from "../addons/tw-disable-cloud-variables/_manifest_entry.js"; import _tw_disable_compiler from "../addons/tw-disable-compiler/_manifest_entry.js"; import _editor_stepping from "../addons/editor-stepping/_manifest_entry.js"; +import _boxy_assistant from "../addons/boxy-assistant/_manifest_entry.js"; import santa from "../addons/santa/_manifest_entry.js"; export default { santa, @@ -162,4 +163,5 @@ export default { "tw-disable-cloud-variables": _tw_disable_cloud_variables, "tw-disable-compiler": _tw_disable_compiler, "editor-stepping": _editor_stepping, + "boxy-assistant": _boxy_assistant, }; diff --git a/test/unit/addons/boxy-assistant.test.js b/test/unit/addons/boxy-assistant.test.js new file mode 100644 index 000000000..ad5e6fd1d --- /dev/null +++ b/test/unit/addons/boxy-assistant.test.js @@ -0,0 +1,53 @@ +/** + * @jest-environment jsdom + */ + +describe('Boxy Assistant Addon', () => { + test('addon manifest exists and has correct structure', () => { + const manifest = require('../../../src/addons/addons/boxy-assistant/_manifest_entry.js').default; + + expect(manifest).toBeDefined(); + expect(manifest.name).toBe('Boxy AI Assistant'); + expect(manifest.editorOnly).toBe(true); + expect(manifest.noTranslations).toBe(true); + expect(manifest.enabledByDefault).toBe(false); + expect(manifest.tags).toContain('featured'); + expect(manifest.tags).toContain('new'); + }); + + test('addon has required files defined', () => { + const manifest = require('../../../src/addons/addons/boxy-assistant/_manifest_entry.js').default; + + expect(manifest.userscripts).toBeDefined(); + expect(manifest.userscripts.length).toBe(1); + expect(manifest.userscripts[0].url).toBe('userscript.js'); + + expect(manifest.userstyles).toBeDefined(); + expect(manifest.userstyles.length).toBe(1); + expect(manifest.userstyles[0].url).toBe('style.css'); + }); + + test('addon is registered in addons.js', () => { + const addonsModule = require('../../../src/addons/addons.js'); + + expect(addonsModule.addons).toContain('boxy-assistant'); + expect(addonsModule.newAddons).toContain('boxy-assistant'); + }); + + test('addon has proper credits', () => { + const manifest = require('../../../src/addons/addons/boxy-assistant/_manifest_entry.js').default; + + expect(manifest.credits).toBeDefined(); + expect(manifest.credits.length).toBeGreaterThan(0); + expect(manifest.credits[0].name).toBe('supervoidcoder'); + }); + + test('addon description is informative', () => { + const manifest = require('../../../src/addons/addons/boxy-assistant/_manifest_entry.js').default; + + expect(manifest.description).toBeDefined(); + expect(manifest.description.length).toBeGreaterThan(50); + expect(manifest.description).toContain('AI'); + expect(manifest.description).toContain('Boxy'); + }); +});