// // End-to-end: activates the real extension against a real git checkout and a // canned Gitea instance, then renders every sidebar section and drives the // commands that touch the filesystem or the API. // const { EXTENSION_ROOT } = require("./nova-stub.js"); const fake = require("./fake-gitea.js"); const { Checker } = require("./assert.js"); const path = require("node:path"); const fs = require("node:fs"); const os = require("node:os"); const { execFileSync } = require("node:child_process"); const script = (...parts) => path.join(EXTENSION_ROOT, "Scripts", ...parts); const t = new Checker(); // -- fixture --------------------------------------------------------------- const WORKSPACE = fs.mkdtempSync(path.join(os.tmpdir(), "gitea-nova-test-")); const REPO = path.join(WORKSPACE, "widget"); function buildRepository() { fs.mkdirSync(path.join(REPO, "src"), { recursive: true }); fs.writeFileSync( path.join(REPO, "src", "login.js"), Array.from({ length: 20 }, (_, i) => `// line ${i + 1}`).join("\n") + "\n", ); fs.writeFileSync(path.join(REPO, "README.md"), "# widget\n"); const git = (...args) => execFileSync("git", args, { cwd: REPO, encoding: "utf-8" }); git("init", "-q", "-b", "main"); git("config", "user.email", "test@example.com"); git("config", "user.name", "Test"); git("remote", "add", "origin", "git@gitea.test:acme/widget.git"); git("add", "-A"); git("commit", "-qm", "initial"); git("checkout", "-qb", "feature/login"); } buildRepository(); nova.workspace.path = WORKSPACE; nova.config.set("gitea.baseUrl", "https://gitea.test"); nova.credentials.setPassword("unsupervised.giteanova", "https://gitea.test", "tok_test"); // -- helpers --------------------------------------------------------------- /** Renders a provider's tree to indented text, expanding lazy rows as it goes. */ async function render(provider, node = null, depth = 0, lines = []) { for (const child of await provider.getChildren(node)) { const item = provider.getTreeItem(child); const description = item.descriptiveText ? ` [${item.descriptiveText}]` : ""; lines.push(`${" ".repeat(depth)}${item.name}${description}`); if (depth < 3 && (child.children || child.load)) { await render(provider, child, depth + 1, lines); } } return lines; } /** Every command name the manifest exposes to the user. */ function declaredCommands(manifest) { const declared = new Set(); for (const list of Object.values(manifest.commands || {})) { for (const entry of list) if (entry.command) declared.add(entry.command); } for (const sidebar of manifest.sidebars || []) { for (const section of sidebar.sections || []) { const entries = [...(section.headerCommands || []), ...(section.contextCommands || [])]; for (const entry of entries) if (entry.command) declared.add(entry.command); } } const walk = (entry) => { if (entry.command) declared.add(entry.command); for (const child of entry.children || []) walk(child); }; for (const entry of manifest.config || []) walk(entry); return declared; } // -- suite ----------------------------------------------------------------- (async () => { const main = require(script("main.js")); main.activate(); await new Promise((resolve) => setTimeout(resolve, 400)); // let activation settle const store = require(script("controllers", "selection.js")).store(); const views = Object.fromEntries(global.__treeViews.map((view) => [view.id, view])); t.section("discovery"); console.log( store.repos .map((r) => `${r.fullName} @ ${r.baseUrl} branch=${r.branch}`) .join("\n") || "(none)", ); t.check("one repository discovered", store.repos.length === 1, `got ${store.repos.length}`); t.check("routed to the matching instance", store.repos[0]?.baseUrl === "https://gitea.test"); t.check("branch read from git", store.repos[0]?.branch === "feature/login", store.repos[0]?.branch); t.check( "instance reported healthy", store.instanceStatus.get("https://gitea.test")?.ok === true, JSON.stringify(store.instanceStatus.get("https://gitea.test")), ); for (const id of ["gitea.branch", "gitea.workflows", "gitea.pullRequests", "gitea.settings"]) { t.section(id); const lines = await render(views[id].dataProvider); console.log(lines.join("\n")); t.check(`${id} renders rows`, lines.length > 0); t.check( `${id} renders no error rows`, !lines.some((line) => line.includes("⚠")), lines.filter((line) => line.includes("⚠")).join(" | "), ); } t.section("branch filtering"); const branchLines = await render(views["gitea.branch"].dataProvider); t.check("shows the branch's pull request", branchLines.some((l) => l.includes("#7 Add login form"))); t.check("hides another branch's pull request", !branchLines.some((l) => l.includes("#6 Bump deps"))); t.check("hides another branch's run", !branchLines.some((l) => l.includes("Nightly"))); t.section("workflow grouping"); const workflowLines = await render(views["gitea.workflows"].dataProvider); t.check( "grouped by workflow file", workflowLines.some((l) => l.trim().startsWith("ci.yml")) && workflowLines.some((l) => l.trim().startsWith("nightly.yml")), ); t.check("jobs load under a run", workflowLines.some((l) => l.includes("build"))); t.check("steps load under a job", workflowLines.some((l) => l.includes("Checkout"))); t.check("artifacts listed under a run", workflowLines.some((l) => l.includes("coverage"))); t.section("inline review comments"); const { ReviewCommentsController } = require(script("controllers", "reviewComments.js")); const comments = new ReviewCommentsController(store); await comments.reload({ force: true }); const annotated = [...comments.collection.map.entries()]; for (const [uri, issues] of annotated) { console.log(uri); for (const issue of issues) console.log(` line ${issue.line} [${issue.source}] ${issue.message}`); } t.check("exactly one file annotated", annotated.length === 1, `got ${annotated.length}`); t.check("annotation lands on the commented file", annotated[0]?.[0].endsWith("/src/login.js")); // The hunk adds three lines after one context line starting at new line 10, // so the commented line — the hunk's last — is line 14. t.check("line derived from the diff hunk", annotated[0]?.[1][0].line === 14, annotated[0]?.[1][0].line); t.check("request-changes comment raised as an error", annotated[0]?.[1][0].severity === IssueSeverity.Error); comments.dispose(); t.section("manifest and command registration"); const manifest = JSON.parse(fs.readFileSync(path.join(EXTENSION_ROOT, "extension.json"), "utf-8")); const declared = declaredCommands(manifest); const registered = global.__commands; const missing = [...declared].filter((name) => !registered.has(name)); t.check("every command the manifest offers is registered", missing.length === 0, missing.join(", ")); // gitea.openReviewComment is deliberately absent: it is a TreeItem.command // for double-click, never a menu entry. const extra = [...registered.keys()].filter((name) => !declared.has(name)); t.equals("only the double-click handler is manifest-free", extra, ["gitea.openReviewComment"]); t.section("command behaviour"); const branch = views["gitea.branch"]; const roots = await branch.dataProvider.getChildren(null); const pullFolder = roots.find((n) => n.kind === "prFolder"); const pullNode = (await branch.dataProvider.getChildren(pullFolder)).find((n) => n.kind === "pr"); branch.selection = [pullNode]; await registered.get("gitea.copyUrl")(branch); t.check( "copy URL uses the pull request's own page", global.__clipboard === "https://gitea.test/acme/widget/pulls/7", global.__clipboard, ); await registered.get("gitea.openPullRequestDiff")(branch); t.check("diff saved into the repository", fs.existsSync(path.join(REPO, ".tmp", "gitea-diffs", "pr-7.diff"))); await registered.get("gitea.openPullRequestOverview")(branch); const overview = global.__opened?.untitled?.content ?? ""; t.check("overview headed with the pull request title", overview.includes("# #7 Add login form")); t.check("overview lists reviews", overview.includes("alice") && overview.includes("request_changes")); t.check("overview lists changed files", overview.includes("src/login.js")); const runsFolder = roots.find((n) => n.kind === "runsFolder"); const runNode = (await branch.dataProvider.getChildren(runsFolder)).find((n) => n.kind === "run"); const jobNode = (await branch.dataProvider.getChildren(runNode)).find((n) => n.kind === "job"); branch.selection = [jobNode]; await registered.get("gitea.viewJobLogs")(branch); const logsDir = path.join(REPO, ".tmp", "gitea-logs"); const logFiles = fs.existsSync(logsDir) ? fs.readdirSync(logsDir) : []; t.check("job log written into the repository", logFiles.length === 1, logFiles.join(", ")); const workflows = views["gitea.workflows"]; const ciGroup = (await workflows.dataProvider.getChildren(null)).find( (n) => n.kind === "workflow" && n.name === "ci.yml", ); const finishedRun = (await workflows.dataProvider.getChildren(ciGroup)).find( (n) => n.data.run.id === 101, ); const artifactsFolder = (await workflows.dataProvider.getChildren(finishedRun)).find( (n) => n.kind === "artifactsFolder", ); workflows.selection = [(await workflows.dataProvider.getChildren(artifactsFolder))[0]]; await registered.get("gitea.downloadArtifact")(workflows); const zip = path.join(REPO, ".tmp", "gitea-artifacts", "acme", "widget", "101", "coverage.zip"); t.check("artifact downloaded to the configured path", fs.existsSync(zip), zip); t.section("secrets and variables"); const settingsLines = await render(views["gitea.settings"].dataProvider); console.log(settingsLines.join("\n")); t.check("secret listed", settingsLines.some((l) => l.includes("DEPLOY_KEY"))); t.check("variable listed", settingsLines.some((l) => l.includes("REGISTRY"))); t.section("write operations"); global.__messages.length = 0; workflows.selection = [finishedRun]; await registered.get("gitea.rerunRun")(workflows); t.check( "re-run reported as requested", global.__messages.some(([kind, message]) => kind === "info" && message.includes("Re-run")), JSON.stringify(global.__messages), ); global.__messages.length = 0; global.__inputAnswer = "Looks good to me"; branch.selection = [pullNode]; await registered.get("gitea.reviewApprove")(branch); t.check( "approval submitted", global.__messages.some(([kind, message]) => kind === "info" && message.includes("Review submitted")), JSON.stringify(global.__messages), ); t.section("the sidebar keeps its shape across refreshes"); // A poll that rebuilds every row used to close whatever the user had open. const workflowsView = views["gitea.workflows"]; const workflowsProvider = workflowsView.dataProvider; const group = (await workflowsProvider.getChildren(null)).find( (n) => n.kind === "workflow" && n.name === "ci.yml", ); const openMe = (await workflowsProvider.getChildren(group)).find((n) => n.data.run.id === 101); t.equals( "a run starts collapsed", workflowsProvider.getTreeItem(openMe).collapsibleState, TreeItemCollapsibleState.Collapsed, ); workflowsView.expand(group); workflowsView.expand(openMe); // What a refresh does: throw the built tree away and rebuild it. workflowsProvider.invalidate(); const rebuiltGroup = (await workflowsProvider.getChildren(null)).find( (n) => n.kind === "workflow" && n.name === "ci.yml", ); const rebuiltRun = (await workflowsProvider.getChildren(rebuiltGroup)).find( (n) => n.data.run.id === 101, ); t.check("the rebuilt run is a different object", rebuiltRun !== openMe); t.equals("but carries the same identifier", rebuiltRun.identifier, openMe.identifier); t.equals( "and is still open", workflowsProvider.getTreeItem(rebuiltRun).collapsibleState, TreeItemCollapsibleState.Expanded, ); t.equals( "its parent too", workflowsProvider.getTreeItem(rebuiltGroup).collapsibleState, TreeItemCollapsibleState.Expanded, ); workflowsView.collapse(rebuiltRun); workflowsProvider.invalidate(); const closedAgain = (await workflowsProvider.getChildren( (await workflowsProvider.getChildren(null)).find((n) => n.name === "ci.yml"), )).find((n) => n.data.run.id === 101); t.equals( "closing it sticks too", workflowsProvider.getTreeItem(closedAgain).collapsibleState, TreeItemCollapsibleState.Collapsed, ); // A section that opens by default must still be closable. const branchProvider2 = views["gitea.branch"].dataProvider; const folder = (await branchProvider2.getChildren(null)).find((n) => n.kind === "prFolder"); t.equals( "a default-open folder starts open", branchProvider2.getTreeItem(folder).collapsibleState, TreeItemCollapsibleState.Expanded, ); views["gitea.branch"].collapse(folder); branchProvider2.invalidate(); const folderAgain = (await branchProvider2.getChildren(null)).find((n) => n.kind === "prFolder"); t.equals( "and stays closed once the user closes it", branchProvider2.getTreeItem(folderAgain).collapsibleState, TreeItemCollapsibleState.Collapsed, ); t.section("idle polls do not redraw"); const first = store.signature(); t.equals("an unchanged store yields an unchanged signature", store.signature(), first); const trackedRuns = store.runsFor(store.repos[0].key).runs; const finished = trackedRuns.find((run) => run.id === 103); const previousConclusion = finished.conclusion; finished.conclusion = "success"; t.check("a changed run conclusion changes it", store.signature() !== first); finished.conclusion = previousConclusion; t.equals("and restoring it restores the signature", store.signature(), first); const branchFilterBefore = store.signature(); nova.workspace.config.set("gitea.branchFilter", "all"); t.check("changing the branch filter changes it", store.signature() !== branchFilterBefore); nova.workspace.config.set("gitea.branchFilter", "current"); t.section("api paths exercised"); console.log([...new Set(fake.calls.map((call) => call.split("?")[0]))].sort().join("\n")); main.deactivate(); fs.rmSync(WORKSPACE, { recursive: true, force: true }); t.finish("INTEGRATION CHECKS"); })().catch((error) => { console.error("HARNESS ERROR", error); fs.rmSync(WORKSPACE, { recursive: true, force: true }); process.exit(2); });