mirror of
https://github.com/alphaXiv/OpenResearch.git
synced 2026-11-11 09:59:34 +08:00
- Clearer model selection - Faster new chat shortcuts - Cleaner session previews - Easier sidebar navigation - More reliable unsent drafts - Safer message retries - Smoother menus and dialogs - Better small-window layouts - More useful tooltips - Improved experiment visibility - More reliable release builds Assisted-by: gpt-6.1-terra Assisted-by: gpt-6.1-sol Assisted-by: claude-sonnet-5.5
223 lines
10 KiB
JavaScript
223 lines
10 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import test from "node:test";
|
|
import React from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import ts from "typescript";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const source = readFileSync(new URL("../src/components/SettingsPage.tsx", import.meta.url), "utf8");
|
|
const compiled = ts.transpileModule(source, {
|
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX },
|
|
}).outputText;
|
|
|
|
// The queries GitTab reads, replaced per test; every other query has no data.
|
|
let queries = {};
|
|
let clicks = new Map();
|
|
let menuOpen = false;
|
|
let state = [];
|
|
let stateIndex = 0;
|
|
let rerender;
|
|
const PopoverContext = React.createContext(null);
|
|
let enableCalls = [];
|
|
const text = (children) => React.Children.toArray(children).filter((child) => typeof child === "string").join("").trim();
|
|
// Every other app module is a stub: its exports render nothing and return nothing.
|
|
const stub = new Proxy({}, { get: (_, name) => (name === "__esModule" ? false : () => null) });
|
|
const queryFactories = new Proxy({}, { get: (_, name) => (...args) => ({ queryKey: [name, ...args] }) });
|
|
const mocks = {
|
|
react: { ...React, useState: (initial) => {
|
|
const index = stateIndex++;
|
|
if (!(index in state)) state[index] = initial;
|
|
return [state[index], (value) => {
|
|
state[index] = typeof value === "function" ? value(state[index]) : value;
|
|
rerender();
|
|
}];
|
|
} },
|
|
"@base-ui/react/popover": { Popover: {
|
|
Root: ({ open, onOpenChange, children }) => {
|
|
menuOpen = open;
|
|
return React.createElement(PopoverContext.Provider, { value: { open, onOpenChange } }, children);
|
|
},
|
|
Trigger: ({ render, children, ...props }) => {
|
|
const { open, onOpenChange } = React.useContext(PopoverContext);
|
|
return React.cloneElement(render, { ...props, "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => onOpenChange(!open) }, children);
|
|
},
|
|
Portal: ({ children }) => children,
|
|
Positioner: ({ children }) => children,
|
|
Popup: ({ children, ...props }) => React.useContext(PopoverContext).open
|
|
? React.createElement("div", { ...props, role: "dialog" }, children) : null,
|
|
} },
|
|
"@tanstack/react-query": {
|
|
useQuery: (options) => queries[options?.queryKey?.[0]] ?? { data: undefined },
|
|
useMutation: () => ({}),
|
|
useQueries: () => [],
|
|
},
|
|
"../queries/settings": queryFactories,
|
|
"../queries/projects": queryFactories,
|
|
"../queries/files": queryFactories,
|
|
"../paraglide/messages.js": { m: new Proxy({}, { get: (_, name) => () => String(name) }) },
|
|
"../paraglide/runtime.js": { getLocale: () => "en", isLocale: () => true },
|
|
"../i18n": { ltr: String },
|
|
"../api": new Proxy({}, { get: (_, name) => name === "enableProjectGithub" ? async (...args) => {
|
|
enableCalls.push(args);
|
|
return { project: { id: "project-1" }, git: signedIn };
|
|
} : stub[name] }),
|
|
"./ui/cn": { cn: (...names) => names.filter(Boolean).join(" ") },
|
|
// Plain elements, keeping each button's click handler by its label.
|
|
"./ui": new Proxy({}, {
|
|
get: (_, name) => {
|
|
if (name === "Button" || name === "MenuItem") return ({ children, onClick, disabled }) => {
|
|
clicks.set(text(children), onClick);
|
|
return React.createElement("button", { disabled }, children);
|
|
};
|
|
if (name === "Badge") return ({ children }) => React.createElement("span", null, children);
|
|
if (name === "Input") return () => React.createElement("input");
|
|
return stub[name];
|
|
},
|
|
}),
|
|
};
|
|
const exports = {};
|
|
new Function("require", "exports", compiled)(
|
|
(name) => mocks[name] ?? (name.startsWith(".") ? stub : require(name)),
|
|
exports,
|
|
);
|
|
|
|
const gitStatus = (github) => ({
|
|
path: "/work/project",
|
|
gitVersion: "2.50.0",
|
|
initialized: true,
|
|
baselineBranch: "main",
|
|
currentBranch: "main",
|
|
clean: true,
|
|
remotes: [],
|
|
identity: { name: null, email: null, nameSource: null, emailSource: null },
|
|
github: { ghInstalled: true, authenticated: false, enabled: false, owner: "", repo: "", url: null, syncStatus: null, ...github },
|
|
});
|
|
const signedIn = gitStatus({ authenticated: true });
|
|
const signedOut = gitStatus({ authenticated: false });
|
|
|
|
function renderGitTab({ status, statusError = null, account = { data: { login: "example-user" }, isFetching: false }, organizations = { data: { organizations: ["research-org"] }, isFetching: false }, access = { data: { canPush: true } }, open = false }) {
|
|
const refetches = [];
|
|
state = [];
|
|
enableCalls = [];
|
|
queries = {
|
|
getProjectGitStatusQuery: { data: status, error: statusError },
|
|
repoAccessQuery: { ...access, refetch: async () => { refetches.push("access"); } },
|
|
githubAccountQuery: { ...account, refetch: async () => { refetches.push("account"); } },
|
|
githubOrganizationsQuery: { ...organizations, refetch: async () => { refetches.push("organizations"); } },
|
|
};
|
|
clicks = new Map();
|
|
let html;
|
|
rerender = () => {
|
|
stateIndex = 0;
|
|
html = renderToStaticMarkup(exports.GitTab({
|
|
project: { id: "project-1", name: "Project" },
|
|
onProjectUpdate: () => {},
|
|
}));
|
|
};
|
|
rerender();
|
|
if (open) clicks.get("repository_enable_syncing")();
|
|
return { html, refetches };
|
|
}
|
|
|
|
const buttonLabels = (html) => [...html.matchAll(/<button[^>]*>([\s\S]*?)<\/button>/g)].map((match) => match[1].replaceAll(/<[^>]*>/g, "").trim());
|
|
const errors = (html) => [...html.matchAll(/<div class="error">([^<]*)<\/div>/g)].map((match) => match[1]);
|
|
const settle = () => new Promise((resolve) => setImmediate(resolve));
|
|
|
|
test("signed-out projects cannot enable GitHub syncing", () => {
|
|
assert.deepEqual(buttonLabels(renderGitTab({ status: signedOut }).html), []);
|
|
});
|
|
|
|
test("a local-only project opens a destination picker without creating a repository", () => {
|
|
const { html } = renderGitTab({ status: signedIn });
|
|
assert.equal(html.includes('role="dialog"'), false);
|
|
assert.equal(html.includes("settings_github_new_repository_destination"), false);
|
|
clicks.get("repository_enable_syncing")({ currentTarget: null });
|
|
assert.equal(menuOpen, true);
|
|
assert.deepEqual(enableCalls, []);
|
|
});
|
|
|
|
test("choosing an organization enables only the selected project under that owner", async () => {
|
|
const { html } = renderGitTab({ status: signedIn, open: true });
|
|
assert.equal(html.includes('role="dialog"'), true);
|
|
assert.deepEqual(buttonLabels(html), ["repository_enable_syncing", "example-user", "research-org"]);
|
|
clicks.get("research-org")();
|
|
await settle();
|
|
assert.deepEqual(enableCalls, [["project-1", "research-org"]]);
|
|
assert.equal(menuOpen, false);
|
|
});
|
|
|
|
test("choosing the personal account uses the authenticated viewer", async () => {
|
|
renderGitTab({ status: signedIn, open: true });
|
|
clicks.get("example-user")();
|
|
await settle();
|
|
assert.deepEqual(enableCalls, [["project-1", undefined]]);
|
|
});
|
|
|
|
test("an existing repository resumes syncing without a destination picker", async () => {
|
|
renderGitTab({ status: gitStatus({ authenticated: true, owner: "existing-org", repo: "paper" }) });
|
|
clicks.get("repository_enable_syncing")({ currentTarget: null });
|
|
await settle();
|
|
assert.deepEqual(enableCalls, [["project-1", undefined]]);
|
|
assert.equal(menuOpen, false);
|
|
});
|
|
|
|
test("read-only or archived repositories require choosing the replacement owner", async () => {
|
|
const status = gitStatus({ authenticated: true, owner: "upstream", repo: "paper" });
|
|
renderGitTab({ status, access: { data: { canPush: false } } });
|
|
clicks.get("repository_enable_syncing")({ currentTarget: null });
|
|
assert.equal(menuOpen, true);
|
|
assert.deepEqual(enableCalls, []);
|
|
const { html } = renderGitTab({ status, access: { data: { canPush: false } }, open: true });
|
|
assert.ok(html.includes("settings_create_private_github_repository"));
|
|
clicks.get("research-org")();
|
|
await settle();
|
|
assert.deepEqual(enableCalls, [["project-1", "research-org"]]);
|
|
});
|
|
|
|
test("repository access must finish loading before enabling and failures offer retry", async () => {
|
|
const status = gitStatus({ authenticated: true, owner: "upstream", repo: "paper" });
|
|
const pending = renderGitTab({ status, access: { data: undefined } });
|
|
assert.ok(pending.html.includes('disabled=""'));
|
|
assert.ok(buttonLabels(pending.html).includes("common_checking"));
|
|
const failed = renderGitTab({ status, access: { error: new Error("GitHub unavailable") } });
|
|
assert.ok(failed.html.includes('disabled=""'));
|
|
assert.ok(failed.html.includes("GitHub unavailable"));
|
|
clicks.get("app_retry")();
|
|
await settle();
|
|
assert.deepEqual(failed.refetches, ["access"]);
|
|
});
|
|
|
|
test("cached owner choices stay visible without a loading placeholder during refresh", () => {
|
|
const { html } = renderGitTab({ status: signedIn, open: true,
|
|
account: { data: { login: "example-user" }, isFetching: true },
|
|
organizations: { data: { organizations: ["research-org"] }, isFetching: true } });
|
|
assert.equal(html.includes('aria-busy="true"'), false);
|
|
assert.ok(buttonLabels(html).includes("research-org"));
|
|
});
|
|
|
|
test("initial discovery reserves space and a failed account lookup offers retry", async () => {
|
|
const loading = renderGitTab({ status: signedIn, open: true,
|
|
account: { data: undefined, isFetching: true }, organizations: { data: undefined, isFetching: true } });
|
|
assert.equal(loading.html.includes('aria-busy="true"'), true);
|
|
const { refetches } = renderGitTab({ status: signedIn, open: true,
|
|
account: { data: { login: null }, isFetching: false }, organizations: { data: undefined, isFetching: false } });
|
|
clicks.get("app_retry")();
|
|
await settle();
|
|
assert.deepEqual(refetches, ["account", "organizations"]);
|
|
});
|
|
|
|
test("a repository-creation error keeps its organization and SSO guidance", () => {
|
|
const message = "GitHub denied repository creation in 'research-org': HTTP 403: Resource protected by organization SAML enforcement. Authorize GitHub CLI for the organization's SAML SSO, then retry.";
|
|
const { html } = renderGitTab({ status: signedIn, statusError: new Error(message) });
|
|
|
|
assert.deepEqual(errors(html), [message.replaceAll("'", "'")]);
|
|
});
|
|
|
|
test("a push rejected with 403 keeps the write-access explanation", () => {
|
|
const { html } = renderGitTab({ status: signedIn, statusError: new Error("remote: Permission to owner/repo.git denied. The requested URL returned error: 403") });
|
|
|
|
assert.deepEqual(errors(html), ["settings_github_permission_error"]);
|
|
});
|