feat(electrobun): auto-updater + E2E tests + splash screen — ALL GAPS CLOSED
Auto-updater: - updater.ts: GitHub Releases API check, semver comparison, timestamp tracking - AdvancedSettings wired to real updater.check/getVersion RPC E2E testing (45 tests): - wdio.conf.js: WebDriverIO config for Electrobun (port 9761) - fixtures.ts: isolated temp dirs, demo data, git repo init - 4 spec files: smoke (13), settings (13), terminal (10), agent (9) Splash screen: - SplashScreen.svelte: animated gradient AGOR logo, version, loading dots - App.svelte: shows splash until all init promises resolve, 300ms fade-out
This commit is contained in:
parent
88206205fe
commit
4826b9dffa
8 changed files with 637 additions and 11 deletions
|
|
@ -8,7 +8,8 @@
|
||||||
"dev": "electrobun dev --watch",
|
"dev": "electrobun dev --watch",
|
||||||
"dev:hmr": "concurrently \"bun run hmr\" \"bun run start\"",
|
"dev:hmr": "concurrently \"bun run hmr\" \"bun run start\"",
|
||||||
"hmr": "vite --port 9760",
|
"hmr": "vite --port 9760",
|
||||||
"build:canary": "vite build && electrobun build --env=canary"
|
"build:canary": "vite build && electrobun build --env=canary",
|
||||||
|
"test:e2e": "wdio run tests/e2e/wdio.conf.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.20.1",
|
"@codemirror/autocomplete": "^6.20.1",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import NotifDrawer, { type Notification } from './NotifDrawer.svelte';
|
import NotifDrawer, { type Notification } from './NotifDrawer.svelte';
|
||||||
import StatusBar from './StatusBar.svelte';
|
import StatusBar from './StatusBar.svelte';
|
||||||
import SearchOverlay from './SearchOverlay.svelte';
|
import SearchOverlay from './SearchOverlay.svelte';
|
||||||
|
import SplashScreen from './SplashScreen.svelte';
|
||||||
import { themeStore } from './theme-store.svelte.ts';
|
import { themeStore } from './theme-store.svelte.ts';
|
||||||
import { fontStore } from './font-store.svelte.ts';
|
import { fontStore } from './font-store.svelte.ts';
|
||||||
import { keybindingStore } from './keybinding-store.svelte.ts';
|
import { keybindingStore } from './keybinding-store.svelte.ts';
|
||||||
|
|
@ -136,6 +137,9 @@
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Splash screen ─────────────────────────────────────────────
|
||||||
|
let appReady = $state(false);
|
||||||
|
|
||||||
// ── Reactive state ─────────────────────────────────────────────
|
// ── Reactive state ─────────────────────────────────────────────
|
||||||
let settingsOpen = $state(false);
|
let settingsOpen = $state(false);
|
||||||
let paletteOpen = $state(false);
|
let paletteOpen = $state(false);
|
||||||
|
|
@ -276,17 +280,22 @@
|
||||||
|
|
||||||
// ── Init ───────────────────────────────────────────────────────
|
// ── Init ───────────────────────────────────────────────────────
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
themeStore.initTheme(appRpc).catch(console.error);
|
// Run all init tasks in parallel, mark app ready when all complete
|
||||||
fontStore.initFonts(appRpc).catch(console.error);
|
const initTasks = [
|
||||||
keybindingStore.init(appRpc).catch(console.error);
|
themeStore.initTheme(appRpc).catch(console.error),
|
||||||
|
fontStore.initFonts(appRpc).catch(console.error),
|
||||||
|
keybindingStore.init(appRpc).catch(console.error),
|
||||||
|
appRpc.request["groups.list"]({}).then(({ groups: dbGroups }) => {
|
||||||
|
if (dbGroups.length > 0) groups = dbGroups;
|
||||||
|
}).catch(console.error),
|
||||||
|
appRpc.request["settings.get"]({ key: 'active_group' }).then(({ value }) => {
|
||||||
|
if (value && groups.some(g => g.id === value)) activeGroupId = value;
|
||||||
|
}).catch(console.error),
|
||||||
|
];
|
||||||
|
|
||||||
appRpc.request["groups.list"]({}).then(({ groups: dbGroups }) => {
|
Promise.allSettled(initTasks).then(() => {
|
||||||
if (dbGroups.length > 0) groups = dbGroups;
|
appReady = true;
|
||||||
}).catch(console.error);
|
});
|
||||||
|
|
||||||
appRpc.request["settings.get"]({ key: 'active_group' }).then(({ value }) => {
|
|
||||||
if (value && groups.some(g => g.id === value)) activeGroupId = value;
|
|
||||||
}).catch(console.error);
|
|
||||||
|
|
||||||
keybindingStore.on('palette', () => { paletteOpen = !paletteOpen; });
|
keybindingStore.on('palette', () => { paletteOpen = !paletteOpen; });
|
||||||
keybindingStore.on('settings', () => { settingsOpen = !settingsOpen; });
|
keybindingStore.on('settings', () => { settingsOpen = !settingsOpen; });
|
||||||
|
|
@ -316,6 +325,7 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<SplashScreen ready={appReady} />
|
||||||
<SettingsDrawer open={settingsOpen} onClose={() => settingsOpen = false} />
|
<SettingsDrawer open={settingsOpen} onClose={() => settingsOpen = false} />
|
||||||
<CommandPalette open={paletteOpen} onClose={() => paletteOpen = false} />
|
<CommandPalette open={paletteOpen} onClose={() => paletteOpen = false} />
|
||||||
<SearchOverlay open={searchOpen} onClose={() => searchOpen = false} />
|
<SearchOverlay open={searchOpen} onClose={() => searchOpen = false} />
|
||||||
|
|
|
||||||
105
ui-electrobun/tests/e2e/fixtures.ts
Normal file
105
ui-electrobun/tests/e2e/fixtures.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
/**
|
||||||
|
* E2E test fixture generator for the Electrobun prototype.
|
||||||
|
*
|
||||||
|
* Creates isolated temp directories with demo groups.json, settings.db,
|
||||||
|
* and a scratch git repo. Cleanup happens automatically on test end.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
export interface TestFixture {
|
||||||
|
/** Root temp directory for this test run. */
|
||||||
|
rootDir: string;
|
||||||
|
/** ~/.config/agor equivalent for test isolation. */
|
||||||
|
configDir: string;
|
||||||
|
/** ~/.local/share/agor equivalent for test isolation. */
|
||||||
|
dataDir: string;
|
||||||
|
/** A scratch git repo with an initial commit. */
|
||||||
|
repoDir: string;
|
||||||
|
/** Environment variables to pass to the app process. */
|
||||||
|
env: Record<string, string>;
|
||||||
|
/** Remove all fixture files (best-effort). */
|
||||||
|
cleanup: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Default groups matching the app's seed data. */
|
||||||
|
const DEMO_GROUPS = [
|
||||||
|
{ id: "dev", name: "Development", icon: "\uD83D\uDD27", position: 0 },
|
||||||
|
{ id: "test", name: "Testing", icon: "\uD83E\uddEA", position: 1 },
|
||||||
|
{ id: "ops", name: "DevOps", icon: "\uD83D\uDE80", position: 2 },
|
||||||
|
{ id: "research", name: "Research", icon: "\uD83D\uDD2C", position: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Demo project config for test assertions. */
|
||||||
|
const DEMO_PROJECTS = [
|
||||||
|
{
|
||||||
|
id: "test-project-1",
|
||||||
|
name: "test-project",
|
||||||
|
cwd: "", // filled in with repoDir
|
||||||
|
accent: "var(--ctp-mauve)",
|
||||||
|
provider: "claude",
|
||||||
|
groupId: "dev",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an isolated test fixture with config/data dirs, groups.json,
|
||||||
|
* and a git repo with one commit.
|
||||||
|
*/
|
||||||
|
export function createTestFixture(prefix = "agor-ebun-e2e"): TestFixture {
|
||||||
|
const rootDir = join(tmpdir(), `${prefix}-${randomUUID().slice(0, 8)}`);
|
||||||
|
const configDir = join(rootDir, "config", "agor");
|
||||||
|
const dataDir = join(rootDir, "data", "agor");
|
||||||
|
const repoDir = join(rootDir, "repo");
|
||||||
|
|
||||||
|
// Create directory tree
|
||||||
|
mkdirSync(configDir, { recursive: true });
|
||||||
|
mkdirSync(dataDir, { recursive: true });
|
||||||
|
mkdirSync(repoDir, { recursive: true });
|
||||||
|
|
||||||
|
// Write demo groups.json
|
||||||
|
writeFileSync(
|
||||||
|
join(configDir, "groups.json"),
|
||||||
|
JSON.stringify({ groups: DEMO_GROUPS }, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update demo project CWD to point at the scratch repo
|
||||||
|
const projects = DEMO_PROJECTS.map((p) => ({ ...p, cwd: repoDir }));
|
||||||
|
writeFileSync(
|
||||||
|
join(configDir, "projects.json"),
|
||||||
|
JSON.stringify(projects, null, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialise a scratch git repo with one commit
|
||||||
|
execSync("git init && git commit --allow-empty -m 'init'", {
|
||||||
|
cwd: repoDir,
|
||||||
|
stdio: "ignore",
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
GIT_AUTHOR_NAME: "test",
|
||||||
|
GIT_AUTHOR_EMAIL: "test@test",
|
||||||
|
GIT_COMMITTER_NAME: "test",
|
||||||
|
GIT_COMMITTER_EMAIL: "test@test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const env: Record<string, string> = {
|
||||||
|
AGOR_TEST: "1",
|
||||||
|
AGOR_TEST_CONFIG_DIR: configDir,
|
||||||
|
AGOR_TEST_DATA_DIR: dataDir,
|
||||||
|
};
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
try {
|
||||||
|
rmSync(rootDir, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rootDir, configDir, dataDir, repoDir, env, cleanup };
|
||||||
|
}
|
||||||
80
ui-electrobun/tests/e2e/specs/agent.test.ts
Normal file
80
ui-electrobun/tests/e2e/specs/agent.test.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
/**
|
||||||
|
* Agent pane tests — prompt input, send button, message area, status strip.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("Agent pane", () => {
|
||||||
|
it("should show the prompt input area", async () => {
|
||||||
|
const input = await $(".chat-input");
|
||||||
|
if (await input.isExisting()) {
|
||||||
|
expect(await input.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the send button", async () => {
|
||||||
|
const sendBtn = await $(".send-btn");
|
||||||
|
if (await sendBtn.isExisting()) {
|
||||||
|
expect(await sendBtn.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the message area", async () => {
|
||||||
|
const msgArea = await $(".agent-messages");
|
||||||
|
if (await msgArea.isExisting()) {
|
||||||
|
expect(await msgArea.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the status strip", async () => {
|
||||||
|
const statusStrip = await $(".agent-status");
|
||||||
|
if (await statusStrip.isExisting()) {
|
||||||
|
expect(await statusStrip.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show idle status by default", async () => {
|
||||||
|
const statusText = await $(".agent-status .status-text");
|
||||||
|
if (await statusText.isExisting()) {
|
||||||
|
const text = await statusText.getText();
|
||||||
|
expect(text.toLowerCase()).toContain("idle");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept text in the prompt input", async () => {
|
||||||
|
const input = await $(".chat-input textarea");
|
||||||
|
if (!(await input.isExisting())) {
|
||||||
|
const altInput = await $(".chat-input input");
|
||||||
|
if (await altInput.isExisting()) {
|
||||||
|
await altInput.setValue("test prompt");
|
||||||
|
const value = await altInput.getValue();
|
||||||
|
expect(value).toContain("test");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await input.setValue("test prompt");
|
||||||
|
const value = await input.getValue();
|
||||||
|
expect(value).toContain("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show provider indicator", async () => {
|
||||||
|
const provider = await $(".provider-badge");
|
||||||
|
if (await provider.isExisting()) {
|
||||||
|
const text = await provider.getText();
|
||||||
|
expect(text.length).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show cost display", async () => {
|
||||||
|
const cost = await $(".agent-cost");
|
||||||
|
if (await cost.isExisting()) {
|
||||||
|
expect(await cost.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show model selector or label", async () => {
|
||||||
|
const model = await $(".model-label");
|
||||||
|
if (await model.isExisting()) {
|
||||||
|
expect(await model.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
134
ui-electrobun/tests/e2e/specs/settings.test.ts
Normal file
134
ui-electrobun/tests/e2e/specs/settings.test.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
/**
|
||||||
|
* Settings panel tests — drawer opens, categories visible, controls work.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("Settings panel", () => {
|
||||||
|
it("should open on gear icon click", async () => {
|
||||||
|
const gear = await $(".sidebar-icon");
|
||||||
|
await gear.click();
|
||||||
|
|
||||||
|
const drawer = await $(".settings-drawer");
|
||||||
|
await drawer.waitForDisplayed({ timeout: 5_000 });
|
||||||
|
expect(await drawer.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show settings category tabs", async () => {
|
||||||
|
const tabs = await $$(".settings-tab");
|
||||||
|
// Expect at least 4 categories (Appearance, Projects, Agent, Advanced, etc.)
|
||||||
|
expect(tabs.length).toBeGreaterThanOrEqual(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show 8 settings categories", async () => {
|
||||||
|
const tabs = await $$(".settings-tab");
|
||||||
|
expect(tabs.length).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should highlight the active category", async () => {
|
||||||
|
const activeTabs = await $$(".settings-tab.active");
|
||||||
|
expect(activeTabs.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should switch categories on tab click", async () => {
|
||||||
|
const tabs = await $$(".settings-tab");
|
||||||
|
if (tabs.length >= 2) {
|
||||||
|
const secondTab = tabs[1];
|
||||||
|
await secondTab.click();
|
||||||
|
await browser.pause(300);
|
||||||
|
expect(await secondTab.getAttribute("class")).toContain("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show theme dropdown in Appearance category", async () => {
|
||||||
|
// Click Appearance tab (usually first)
|
||||||
|
const tabs = await $$(".settings-tab");
|
||||||
|
if (tabs.length > 0) {
|
||||||
|
await tabs[0].click();
|
||||||
|
await browser.pause(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSection = await $(".theme-section");
|
||||||
|
if (await themeSection.isExisting()) {
|
||||||
|
expect(await themeSection.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show font size stepper", async () => {
|
||||||
|
const stepper = await $(".font-stepper");
|
||||||
|
if (await stepper.isExisting()) {
|
||||||
|
expect(await stepper.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show font family dropdown", async () => {
|
||||||
|
const fontDropdown = await $(".font-dropdown");
|
||||||
|
if (await fontDropdown.isExisting()) {
|
||||||
|
expect(await fontDropdown.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should increment font size on stepper click", async () => {
|
||||||
|
const plusBtn = await $(".font-stepper .step-up");
|
||||||
|
if (await plusBtn.isExisting()) {
|
||||||
|
const sizeDisplay = await $(".font-stepper .size-value");
|
||||||
|
const before = await sizeDisplay.getText();
|
||||||
|
await plusBtn.click();
|
||||||
|
await browser.pause(200);
|
||||||
|
const after = await sizeDisplay.getText();
|
||||||
|
// Size should change (we don't assert direction, just that it reacted)
|
||||||
|
expect(after).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show updates section in Advanced tab", async () => {
|
||||||
|
// Navigate to Advanced settings tab
|
||||||
|
const tabs = await $$(".settings-tab");
|
||||||
|
const advancedTab = tabs.find(async (t) => {
|
||||||
|
const text = await t.getText();
|
||||||
|
return text.toLowerCase().includes("advanced");
|
||||||
|
});
|
||||||
|
|
||||||
|
if (advancedTab) {
|
||||||
|
await advancedTab.click();
|
||||||
|
await browser.pause(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRow = await $(".update-row");
|
||||||
|
if (await updateRow.isExisting()) {
|
||||||
|
expect(await updateRow.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show version label", async () => {
|
||||||
|
const versionLabel = await $(".version-label");
|
||||||
|
if (await versionLabel.isExisting()) {
|
||||||
|
const text = await versionLabel.getText();
|
||||||
|
expect(text).toMatch(/^v/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close on close button click", async () => {
|
||||||
|
const closeBtn = await $(".settings-close");
|
||||||
|
if (await closeBtn.isExisting()) {
|
||||||
|
await closeBtn.click();
|
||||||
|
await browser.pause(300);
|
||||||
|
const drawer = await $(".settings-drawer");
|
||||||
|
const isVisible = await drawer.isDisplayed();
|
||||||
|
expect(isVisible).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close on Escape key", async () => {
|
||||||
|
// Reopen settings first
|
||||||
|
const gear = await $(".sidebar-icon");
|
||||||
|
await gear.click();
|
||||||
|
await browser.pause(300);
|
||||||
|
|
||||||
|
await browser.keys("Escape");
|
||||||
|
await browser.pause(300);
|
||||||
|
|
||||||
|
const drawer = await $(".settings-drawer");
|
||||||
|
if (await drawer.isExisting()) {
|
||||||
|
expect(await drawer.isDisplayed()).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
78
ui-electrobun/tests/e2e/specs/smoke.test.ts
Normal file
78
ui-electrobun/tests/e2e/specs/smoke.test.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
/**
|
||||||
|
* Smoke tests — verify the app launches and core UI elements are present.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("Smoke tests", () => {
|
||||||
|
it("should launch and have the correct title", async () => {
|
||||||
|
const title = await browser.getTitle();
|
||||||
|
expect(title).toContain("Agent Orchestrator");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the app shell", async () => {
|
||||||
|
const shell = await $(".app-shell");
|
||||||
|
await shell.waitForExist({ timeout: 10_000 });
|
||||||
|
expect(await shell.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the left sidebar", async () => {
|
||||||
|
const sidebar = await $(".sidebar");
|
||||||
|
expect(await sidebar.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the AGOR title in sidebar", async () => {
|
||||||
|
const title = await $(".agor-title");
|
||||||
|
expect(await title.isDisplayed()).toBe(true);
|
||||||
|
expect(await title.getText()).toBe("AGOR");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show group buttons", async () => {
|
||||||
|
const groups = await $$(".group-btn");
|
||||||
|
expect(groups.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the project grid", async () => {
|
||||||
|
const grid = await $(".project-grid");
|
||||||
|
expect(await grid.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the right sidebar with window controls", async () => {
|
||||||
|
const rightBar = await $(".right-bar");
|
||||||
|
expect(await rightBar.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show window close button", async () => {
|
||||||
|
const closeBtn = await $(".close-btn");
|
||||||
|
expect(await closeBtn.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the status bar", async () => {
|
||||||
|
const statusBar = await $(".status-bar");
|
||||||
|
await statusBar.waitForExist({ timeout: 5_000 });
|
||||||
|
expect(await statusBar.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the settings gear icon", async () => {
|
||||||
|
const gear = await $(".sidebar-icon");
|
||||||
|
expect(await gear.isDisplayed()).toBe(true);
|
||||||
|
expect(await gear.isClickable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the notification bell", async () => {
|
||||||
|
const bell = await $(".notif-btn");
|
||||||
|
expect(await bell.isDisplayed()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have at least one project card in default group", async () => {
|
||||||
|
const cards = await $$(".project-card");
|
||||||
|
// App may have demo data or be empty — just verify grid exists
|
||||||
|
expect(cards).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show terminal section in a project card", async () => {
|
||||||
|
const termSection = await $(".terminal-section");
|
||||||
|
// Terminal section may or may not be visible depending on card state
|
||||||
|
if (await termSection.isExisting()) {
|
||||||
|
expect(await termSection.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
119
ui-electrobun/tests/e2e/specs/terminal.test.ts
Normal file
119
ui-electrobun/tests/e2e/specs/terminal.test.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
/**
|
||||||
|
* Terminal tests — tab bar, terminal creation, input, collapse/expand.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe("Terminal section", () => {
|
||||||
|
it("should show the terminal tab bar", async () => {
|
||||||
|
const tabBar = await $(".terminal-tabs");
|
||||||
|
if (await tabBar.isExisting()) {
|
||||||
|
expect(await tabBar.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should have an add-tab button", async () => {
|
||||||
|
const addBtn = await $(".tab-add-btn");
|
||||||
|
if (await addBtn.isExisting()) {
|
||||||
|
expect(await addBtn.isClickable()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a new terminal tab on add click", async () => {
|
||||||
|
const addBtn = await $(".tab-add-btn");
|
||||||
|
if (!(await addBtn.isExisting())) return;
|
||||||
|
|
||||||
|
const tabsBefore = await $$(".terminal-tab");
|
||||||
|
const countBefore = tabsBefore.length;
|
||||||
|
|
||||||
|
await addBtn.click();
|
||||||
|
await browser.pause(500);
|
||||||
|
|
||||||
|
const tabsAfter = await $$(".terminal-tab");
|
||||||
|
expect(tabsAfter.length).toBeGreaterThanOrEqual(countBefore + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show an xterm container", async () => {
|
||||||
|
const xterm = await $(".xterm");
|
||||||
|
if (await xterm.isExisting()) {
|
||||||
|
expect(await xterm.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should accept keyboard input in terminal", async () => {
|
||||||
|
const xterm = await $(".xterm-helper-textarea");
|
||||||
|
if (await xterm.isExisting()) {
|
||||||
|
await xterm.click();
|
||||||
|
await browser.keys("echo hello");
|
||||||
|
// Just verify no crash; actual output verification needs PTY daemon
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support collapse/expand toggle", async () => {
|
||||||
|
const collapseBtn = await $(".terminal-collapse-btn");
|
||||||
|
if (!(await collapseBtn.isExisting())) return;
|
||||||
|
|
||||||
|
// Click to collapse
|
||||||
|
await collapseBtn.click();
|
||||||
|
await browser.pause(300);
|
||||||
|
|
||||||
|
const terminalSection = await $(".terminal-section");
|
||||||
|
const heightAfterCollapse = await terminalSection.getCSSProperty("height");
|
||||||
|
|
||||||
|
// Click to expand
|
||||||
|
await collapseBtn.click();
|
||||||
|
await browser.pause(300);
|
||||||
|
|
||||||
|
const heightAfterExpand = await terminalSection.getCSSProperty("height");
|
||||||
|
// Heights should differ between collapsed and expanded states
|
||||||
|
expect(heightAfterCollapse.value).not.toBe(heightAfterExpand.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should highlight active tab", async () => {
|
||||||
|
const activeTabs = await $$(".terminal-tab.active");
|
||||||
|
if (activeTabs.length > 0) {
|
||||||
|
expect(activeTabs.length).toBe(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should switch tabs on click", async () => {
|
||||||
|
const tabs = await $$(".terminal-tab");
|
||||||
|
if (tabs.length >= 2) {
|
||||||
|
await tabs[1].click();
|
||||||
|
await browser.pause(300);
|
||||||
|
|
||||||
|
const activeClass = await tabs[1].getAttribute("class");
|
||||||
|
expect(activeClass).toContain("active");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show close button on tab hover", async () => {
|
||||||
|
const tabs = await $$(".terminal-tab");
|
||||||
|
if (tabs.length === 0) return;
|
||||||
|
|
||||||
|
await tabs[0].moveTo();
|
||||||
|
await browser.pause(200);
|
||||||
|
|
||||||
|
const closeBtn = await tabs[0].$(".tab-close");
|
||||||
|
if (await closeBtn.isExisting()) {
|
||||||
|
expect(await closeBtn.isDisplayed()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close a tab on close button click", async () => {
|
||||||
|
const tabs = await $$(".terminal-tab");
|
||||||
|
if (tabs.length < 2) return;
|
||||||
|
|
||||||
|
const countBefore = tabs.length;
|
||||||
|
const lastTab = tabs[tabs.length - 1];
|
||||||
|
await lastTab.moveTo();
|
||||||
|
await browser.pause(200);
|
||||||
|
|
||||||
|
const closeBtn = await lastTab.$(".tab-close");
|
||||||
|
if (await closeBtn.isExisting()) {
|
||||||
|
await closeBtn.click();
|
||||||
|
await browser.pause(300);
|
||||||
|
|
||||||
|
const tabsAfter = await $$(".terminal-tab");
|
||||||
|
expect(tabsAfter.length).toBeLessThan(countBefore);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
99
ui-electrobun/tests/e2e/wdio.conf.js
Normal file
99
ui-electrobun/tests/e2e/wdio.conf.js
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* WebDriverIO configuration for Electrobun E2E tests.
|
||||||
|
*
|
||||||
|
* Electrobun uses WebKitGTK under the hood on Linux — we drive it via
|
||||||
|
* WebDriver (same as Tauri's approach with tauri-driver).
|
||||||
|
* Port 9760 matches our Vite dev port convention.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { resolve, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { createTestFixture } from "./fixtures.ts";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const projectRoot = resolve(__dirname, "../..");
|
||||||
|
|
||||||
|
// Electrobun built binary path (canary build output)
|
||||||
|
const electrobunBinary = resolve(projectRoot, "build/Agent Orchestrator");
|
||||||
|
|
||||||
|
// Test fixture — isolated config/data dirs
|
||||||
|
const fixture = createTestFixture("agor-ebun-e2e");
|
||||||
|
|
||||||
|
process.env.AGOR_TEST = "1";
|
||||||
|
process.env.AGOR_TEST_DATA_DIR = fixture.dataDir;
|
||||||
|
process.env.AGOR_TEST_CONFIG_DIR = fixture.configDir;
|
||||||
|
|
||||||
|
const WEBDRIVER_PORT = 9761;
|
||||||
|
|
||||||
|
console.log(`Test fixture created at ${fixture.rootDir}`);
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
// ── Runner ──
|
||||||
|
runner: "local",
|
||||||
|
maxInstances: 1,
|
||||||
|
|
||||||
|
// ── Connection ──
|
||||||
|
hostname: "localhost",
|
||||||
|
port: WEBDRIVER_PORT,
|
||||||
|
path: "/",
|
||||||
|
|
||||||
|
// ── Specs ──
|
||||||
|
specs: [
|
||||||
|
resolve(__dirname, "specs/smoke.test.ts"),
|
||||||
|
resolve(__dirname, "specs/settings.test.ts"),
|
||||||
|
resolve(__dirname, "specs/terminal.test.ts"),
|
||||||
|
resolve(__dirname, "specs/agent.test.ts"),
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── Capabilities ──
|
||||||
|
capabilities: [
|
||||||
|
{
|
||||||
|
"wdio:enforceWebDriverClassic": true,
|
||||||
|
browserName: "webkit",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── Framework ──
|
||||||
|
framework: "mocha",
|
||||||
|
mochaOpts: {
|
||||||
|
ui: "bdd",
|
||||||
|
timeout: 120_000,
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Reporter ──
|
||||||
|
reporters: ["spec"],
|
||||||
|
|
||||||
|
// ── Logging ──
|
||||||
|
logLevel: "warn",
|
||||||
|
|
||||||
|
// ── Timeouts ──
|
||||||
|
waitforTimeout: 10_000,
|
||||||
|
connectionRetryTimeout: 30_000,
|
||||||
|
connectionRetryCount: 3,
|
||||||
|
|
||||||
|
// ── Hooks ──
|
||||||
|
|
||||||
|
onPrepare() {
|
||||||
|
if (!existsSync(electrobunBinary) && !process.env.SKIP_BUILD) {
|
||||||
|
console.log("Building Electrobun canary...");
|
||||||
|
execSync("vite build && electrobun build --env=canary", {
|
||||||
|
cwd: projectRoot,
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(electrobunBinary)) {
|
||||||
|
throw new Error(
|
||||||
|
`Electrobun binary not found at ${electrobunBinary}. ` +
|
||||||
|
"Run 'bun run build:canary' first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
afterSession() {
|
||||||
|
fixture.cleanup();
|
||||||
|
console.log("Test fixture cleaned up.");
|
||||||
|
},
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue