mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-16 19:03:26 +00:00
Add Azure Connector Namespaces canvas extension 🤖🤖🤖 (#2250)
* Add MCP Connectors (connector-namespaces) canvas extension A Copilot CLI canvas extension for browsing and adding MCP connectors from an Azure Connector Namespace into a Copilot session. Sign-in is dependency-free (OAuth 2.0 auth-code + PKCE via the Azure CLI public client, loopback redirect); network access is restricted to the public Azure Resource Manager endpoint. MIT licensed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update Connector Namespaces canvas extension Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden Connector Namespaces canvas extension Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Sanitize connector icon brand colors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Connector Namespace playground actions Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Replace sandbox skill with native tool Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Clarify connector disconnect action Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Color disconnect action red Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Add connector extension plugin manifest Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Address connector canvas review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Remove legacy connector canvas manifest Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Address remaining connector review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Harden connector review fixes Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Harden connector convergence Address the latest connector review batch across config persistence, executable trust, reauthentication, JSON-RPC transport, smoke safety, and accessibility. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Use native HTTP connector configs Persist Connector Namespace MCP servers as direct HTTPS entries with API-key headers, remove the stdio unwrap proxy, and exercise the native Streamable HTTP path in smoke coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf * Secure persisted connector state Create the Connector Namespace artifacts directory and saved gateway config with private permissions, and align the reduced-motion regression notes with the static fallback behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a986299f-86be-46c3-9562-cbf7d25174cf --------- Co-authored-by: Alex Yang <yangalex@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Screenshot evidence is throwaway, regenerated on demand by shots.mjs.
|
||||
shots/
|
||||
@@ -0,0 +1,112 @@
|
||||
# connector-namespaces preview harness
|
||||
|
||||
A standalone way to **see** every canvas state without launching the Copilot
|
||||
app. It imports the real, pure renderer functions from `../renderer.mjs` and
|
||||
serves each state on a fixed loopback port, with every `/api/*` endpoint stubbed
|
||||
so you can force the states that keep regressing (the connecting spinner and the
|
||||
"Restart your Copilot session…" banner).
|
||||
|
||||
This exists because those two bugs have each shipped multiple times:
|
||||
|
||||
- the sign-in spinner freezing (an unscoped `animation:none` leaking out of the
|
||||
reduced-motion block), and
|
||||
- the restart-banner dismiss button doing nothing (a CSS specificity bug that
|
||||
let `.restart-banner{display:flex}` beat `[hidden]`).
|
||||
|
||||
Both are static CSS facts, so the **deterministic gate is `../renderer.test.mjs`**
|
||||
(run with `node --test`). This harness is the human-visual layer on top of it:
|
||||
load a state in a browser, or capture screenshots with `agent-browser`.
|
||||
|
||||
## Run the preview server
|
||||
|
||||
```sh
|
||||
node extensions/connector-namespaces/preview/server.mjs
|
||||
```
|
||||
|
||||
It binds to `http://127.0.0.1:7331`. Open that URL in any browser. The server is
|
||||
a plain HTTP process (not the JSON-RPC extension provider), so it logs every hit
|
||||
to stdout — that's expected and fine here.
|
||||
|
||||
### State routes
|
||||
|
||||
| URL | State |
|
||||
| --- | --- |
|
||||
| `/` or `/catalog` | Configured catalog (mock gateway + connectors) |
|
||||
| `/setup` | First-run gateway picker (`renderSetupHtml`) |
|
||||
| `/error` | Error screen (`renderErrorHtml`) |
|
||||
|
||||
### State-forcing query flags (on the catalog route)
|
||||
|
||||
The catalog page hydrates from `/api/state` on load, so loading one of these
|
||||
sets the state the very next `/api/state` returns:
|
||||
|
||||
| Flag | Effect |
|
||||
| --- | --- |
|
||||
| `/?restart=1` | `/api/state` returns `pendingRestart:true` → restart banner visible on load |
|
||||
| `/?installed=1` | One connector shows as already installed/connected |
|
||||
|
||||
Flags combine, e.g. `/?installed=1&restart=1`.
|
||||
|
||||
> The active state is a single module-level flag (last catalog load wins). It's a
|
||||
> single-user preview, so just load the page you want, then it's sticky until the
|
||||
> next catalog load.
|
||||
|
||||
### Stubbed endpoints
|
||||
|
||||
`/api/state`, `/api/gateways`, `/api/select-gateway`, `/api/install` (returns
|
||||
`needsConsent` to force the connecting spinner), `/api/finish-install`,
|
||||
`/api/ack-restart` (the dismiss action), `/oauth-status` (stays pending so the
|
||||
modal spinner keeps animating), `/api/uninstall`, `/api/rollback-connection`,
|
||||
and `/api/open-url` (a deliberate **no-op** here — it must never actually launch
|
||||
a browser tab).
|
||||
|
||||
## Capture screenshots (optional)
|
||||
|
||||
The screenshot driver uses [`agent-browser`](https://www.npmjs.com/package/agent-browser),
|
||||
the same headless-Chromium verification tool that `arikbidny/ralph-copilot-cli`
|
||||
uses. It is **not** required — if it isn't installed the driver prints an install
|
||||
hint and exits 0.
|
||||
|
||||
Install it once:
|
||||
|
||||
```sh
|
||||
npm i -g agent-browser && agent-browser install
|
||||
```
|
||||
|
||||
Then, with the server running in another terminal:
|
||||
|
||||
```sh
|
||||
node extensions/connector-namespaces/preview/shots.mjs
|
||||
```
|
||||
|
||||
Screenshots are written to `preview/shots/`:
|
||||
|
||||
- `catalog.png`, `catalog-restart-banner.png`, `catalog-installed.png`,
|
||||
`setup.png`, `error.png` — the static states.
|
||||
- `connecting-spinner.png` — after clicking **Connect**; verify the `.si-spin`
|
||||
ring is mid-rotation, not frozen.
|
||||
- `banner-before-dismiss.png` / `banner-after-dismiss.png` — verify the banner is
|
||||
present in the first and **gone** in the second.
|
||||
|
||||
`preview/shots/` is throwaway visual evidence; it is not committed.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `server.mjs` | Standalone preview server (fixed port 7331) |
|
||||
| `fixtures.mjs` | Deterministic mock subscriptions / gateways / catalog / state |
|
||||
| `shots.mjs` | `agent-browser` screenshot driver (degrades gracefully) |
|
||||
|
||||
## Relationship to the test guard
|
||||
|
||||
`shots.mjs` proves a state *looks* right today and is handy when chasing a new
|
||||
bug. It cannot prove an animation is *running* from a single frame. The
|
||||
regression gate that actually blocks the recurring bugs is the CSS-structure
|
||||
assertion in `../renderer.test.mjs`:
|
||||
|
||||
```sh
|
||||
node --test extensions/connector-namespaces/renderer.test.mjs
|
||||
```
|
||||
|
||||
Keep that green; use this harness to eyeball changes.
|
||||
@@ -0,0 +1,117 @@
|
||||
// Deterministic fixtures for the standalone canvas preview server.
|
||||
//
|
||||
// These mirror the exact response shapes the inline client script in
|
||||
// renderer.mjs expects, so the preview server can drive every canvas state
|
||||
// (setup / catalog / error / connecting-spinner / restart-banner) with no
|
||||
// Copilot app, no ARM, and no real OAuth. Keep these shapes in sync with the
|
||||
// fetch() handlers in renderer.mjs if those response contracts change.
|
||||
|
||||
import { CATEGORY } from "../categories.mjs";
|
||||
|
||||
export const subscriptions = [
|
||||
{ id: "00000000-0000-0000-0000-000000000001", name: "Contoso Production" },
|
||||
{ id: "11111111-1111-1111-1111-111111111111", name: "Contoso Dev/Test" },
|
||||
];
|
||||
|
||||
// /api/gateways?subscriptionId=... -> { gateways: [{ id, name, location }], hasMore }
|
||||
// The client splits id on "/" and reads the segment after "resourceGroups",
|
||||
// so the id must contain a resourceGroups segment.
|
||||
export const gateways = [
|
||||
{
|
||||
id: "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/rg-connectors/providers/Microsoft.ConnectorNamespaces/connectorNamespaces/contoso-ns",
|
||||
name: "contoso-ns",
|
||||
location: "eastus",
|
||||
},
|
||||
{
|
||||
id: "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/rg-shared/providers/Microsoft.ConnectorNamespaces/connectorNamespaces/shared-ns",
|
||||
name: "shared-ns",
|
||||
location: "westus2",
|
||||
},
|
||||
];
|
||||
|
||||
// Active namespace shown in the catalog header (config.gatewayName / resourceGroup).
|
||||
export const config = {
|
||||
subscriptionId: "00000000-0000-0000-0000-000000000001",
|
||||
gatewayName: "contoso-ns",
|
||||
resourceGroup: "rg-connectors",
|
||||
};
|
||||
|
||||
// Catalog tiles. Shape per item: { category, displayName, apiName, description,
|
||||
// iconUri?, brandColor? }. At least one item must be connectable so the
|
||||
// connect -> spinner flow can be exercised.
|
||||
//
|
||||
// The renderer routes items by category: exactly `category === CATEGORY.microsoft`
|
||||
// lands in the Microsoft section, everything else in Partners. Keep a mix of
|
||||
// both here so the preview exercises the full 3-section layout (My MCPs /
|
||||
// Microsoft / Partners) rather than dumping every tile into one section.
|
||||
export const catalog = [
|
||||
{
|
||||
category: CATEGORY.microsoft,
|
||||
displayName: "Microsoft Teams",
|
||||
apiName: "teams",
|
||||
description: "Send messages, manage chats and channels.",
|
||||
brandColor: "#5059c9",
|
||||
},
|
||||
{
|
||||
category: CATEGORY.microsoft,
|
||||
displayName: "Outlook Mail",
|
||||
apiName: "outlook",
|
||||
description: "Read, send, and organize email.",
|
||||
brandColor: "#0a66c2",
|
||||
},
|
||||
{
|
||||
category: CATEGORY.microsoft,
|
||||
displayName: "SharePoint",
|
||||
apiName: "sharepoint",
|
||||
description: "Browse sites, lists, and documents.",
|
||||
brandColor: "#038387",
|
||||
},
|
||||
{
|
||||
category: CATEGORY.partner,
|
||||
displayName: "GitHub",
|
||||
apiName: "github",
|
||||
description: "Manage repos, issues, and pull requests.",
|
||||
brandColor: "#24292e",
|
||||
},
|
||||
{
|
||||
category: CATEGORY.partner,
|
||||
displayName: "Stripe",
|
||||
apiName: "stripe",
|
||||
description: "Payments, customers, and invoices.",
|
||||
brandColor: "#635bff",
|
||||
},
|
||||
];
|
||||
|
||||
// /api/state -> { state: { apiName: InstallState }, pendingRestart }
|
||||
// InstallState: { installed, connectionStatus, inCli, cliPath?, cliScope? }
|
||||
// Default state: nothing installed, no pending restart. The catalog renders
|
||||
// every tile with a "Connect" button.
|
||||
export const stateEmpty = {
|
||||
state: {},
|
||||
pendingRestart: false,
|
||||
};
|
||||
|
||||
// One connector already added (shows "Added" + Remove), restart pending so the
|
||||
// banner is visible on load. Drives both the "added" tile and the banner state.
|
||||
export const stateInstalledRestart = {
|
||||
state: {
|
||||
sharepoint: {
|
||||
installed: true,
|
||||
connectionStatus: "Connected",
|
||||
inCli: true,
|
||||
cliPath: "~/.copilot/mcp-config.json",
|
||||
cliScope: "profile",
|
||||
},
|
||||
},
|
||||
pendingRestart: true,
|
||||
};
|
||||
|
||||
// Install response that forces the connecting flow. needsConsent keeps the
|
||||
// sign-in modal (with the .si-spin spinner) open; /oauth-status then stays
|
||||
// pending so the spinner keeps animating for a screenshot.
|
||||
export const installNeedsConsent = {
|
||||
needsConsent: true,
|
||||
connName: "preview-conn",
|
||||
consentUrl: "http://127.0.0.1:7331/fake-consent",
|
||||
location: "eastus",
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
// Standalone preview server for the connector-namespaces connector catalog.
|
||||
//
|
||||
// Renders every canvas state with no Copilot app, no ARM, and no real OAuth by
|
||||
// importing the *pure* HTML builders from renderer.mjs and stubbing every
|
||||
// /api/* endpoint the inline client script calls. Point any browser (or the
|
||||
// agent-browser driver in shots.mjs) at it to see exactly what ships.
|
||||
//
|
||||
// Run: node extensions/connector-namespaces/preview/server.mjs
|
||||
// Then open http://127.0.0.1:7331/ (catalog), /setup, /error.
|
||||
//
|
||||
// This process is NOT the JSON-RPC extension provider, so console.log here is
|
||||
// fine and intentional — it is how you watch which stubbed endpoints get hit.
|
||||
|
||||
import { createServer } from "node:http";
|
||||
|
||||
import {
|
||||
renderCatalogHtml,
|
||||
renderSetupHtml,
|
||||
renderErrorHtml,
|
||||
} from "../renderer.mjs";
|
||||
import * as fixtures from "./fixtures.mjs";
|
||||
|
||||
const HOST = "127.0.0.1";
|
||||
const PORT = 7331;
|
||||
const INSTANCE = "preview";
|
||||
|
||||
// Whatever /api/state should report next. The catalog route updates this from
|
||||
// its query flags so a page load can force the banner / "added" tile on, and a
|
||||
// real Connect click flips pendingRestart on via showRestartBanner().
|
||||
let activeState = fixtures.stateEmpty;
|
||||
|
||||
function selectState(query) {
|
||||
const restart = query.get("restart") === "1";
|
||||
const installed = query.get("installed") === "1";
|
||||
if (restart && installed) return fixtures.stateInstalledRestart;
|
||||
if (installed) return { state: fixtures.stateInstalledRestart.state, pendingRestart: false };
|
||||
if (restart) return { state: {}, pendingRestart: true };
|
||||
return fixtures.stateEmpty;
|
||||
}
|
||||
|
||||
function sendHtml(res, body) {
|
||||
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function sendJson(res, obj, status = 200) {
|
||||
res.statusCode = status;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
async function readBody(req) {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) chunks.push(chunk);
|
||||
if (!chunks.length) return {};
|
||||
try {
|
||||
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://${HOST}:${PORT}`);
|
||||
const path = url.pathname;
|
||||
const q = url.searchParams;
|
||||
// Strip CR/LF/tab so a crafted request line can't forge extra log entries.
|
||||
console.log(`${req.method} ${req.url}`.replace(/[\r\n\t]/g, " "));
|
||||
|
||||
// --- Page routes ---------------------------------------------------------
|
||||
if (req.method === "GET" && (path === "/" || path === "/catalog")) {
|
||||
activeState = selectState(q);
|
||||
return sendHtml(
|
||||
res,
|
||||
renderCatalogHtml(INSTANCE, fixtures.catalog, {
|
||||
filter: q.get("filter") || "",
|
||||
category: q.get("category") || "all",
|
||||
source: q.get("source") || "",
|
||||
config: fixtures.config,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (req.method === "GET" && path === "/setup") {
|
||||
return sendHtml(res, renderSetupHtml(fixtures.subscriptions));
|
||||
}
|
||||
if (req.method === "GET" && path === "/error") {
|
||||
return sendHtml(res, renderErrorHtml(q.get("message") || "Something went wrong loading connectors."));
|
||||
}
|
||||
if (req.method === "GET" && path === "/fake-consent") {
|
||||
return sendHtml(res, "<!doctype html><meta charset=utf-8><title>Consent</title><body style=\"font-family:sans-serif;padding:2rem\">Fake Microsoft consent page (preview). Close this tab.</body>");
|
||||
}
|
||||
|
||||
// --- Stubbed API endpoints ----------------------------------------------
|
||||
if (req.method === "GET" && path === "/api/state") {
|
||||
return sendJson(res, activeState);
|
||||
}
|
||||
if (req.method === "GET" && path === "/api/gateways") {
|
||||
return sendJson(res, { gateways: fixtures.gateways, hasMore: false });
|
||||
}
|
||||
if (req.method === "GET" && path === "/oauth-status") {
|
||||
// Stay pending forever so the connecting spinner keeps animating for a
|
||||
// screenshot. Flip to { done: true } if you want the full success flow.
|
||||
return sendJson(res, { done: false });
|
||||
}
|
||||
|
||||
if (req.method === "POST") {
|
||||
await readBody(req);
|
||||
switch (path) {
|
||||
case "/api/select-gateway":
|
||||
return sendJson(res, { ok: true });
|
||||
case "/api/install":
|
||||
return sendJson(res, fixtures.installNeedsConsent);
|
||||
case "/api/finish-install":
|
||||
activeState = { ...activeState, pendingRestart: true };
|
||||
return sendJson(res, { ok: true });
|
||||
case "/api/ack-restart":
|
||||
activeState = { ...activeState, pendingRestart: false };
|
||||
return sendJson(res, { ok: true });
|
||||
case "/api/uninstall":
|
||||
return sendJson(res, { ok: true });
|
||||
case "/api/open-url":
|
||||
// Preview no-op: do NOT actually launch a browser tab.
|
||||
return sendJson(res, { ok: true });
|
||||
case "/api/rollback-connection":
|
||||
return sendJson(res, { ok: true });
|
||||
default:
|
||||
return sendJson(res, { error: `unstubbed POST ${path}` }, 404);
|
||||
}
|
||||
}
|
||||
|
||||
res.statusCode = 404;
|
||||
res.end("not found");
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
if (err.code === "EADDRINUSE") {
|
||||
console.error(`Port ${PORT} is already in use. Stop the other process or change PORT in server.mjs.`);
|
||||
process.exit(1);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`canvas preview server: http://${HOST}:${PORT}/`);
|
||||
console.log(" / catalog (empty state)");
|
||||
console.log(" /?restart=1 catalog with restart banner visible");
|
||||
console.log(" /?installed=1 catalog with one connector added");
|
||||
console.log(" /setup namespace picker");
|
||||
console.log(" /error error state");
|
||||
console.log("Press Ctrl+C to stop.");
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
// agent-browser screenshot driver for the canvas preview server.
|
||||
//
|
||||
// Captures every canvas state to ./shots/ and drives the two interaction flows
|
||||
// that keep regressing:
|
||||
// 1. catalog -> click Connect -> sign-in modal with the spinning .si-spin
|
||||
// 2. restart banner visible -> click dismiss -> banner gone
|
||||
//
|
||||
// Requires the preview server to be running:
|
||||
// node extensions/connector-namespaces/preview/server.mjs
|
||||
// And agent-browser installed:
|
||||
// npm i -g agent-browser && agent-browser install
|
||||
//
|
||||
// If agent-browser is not installed, this script prints how to install it and
|
||||
// exits 0 (so it never breaks an unattended run). This is a visual-evidence
|
||||
// helper; the deterministic regression gate is renderer.test.mjs.
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const SHOTS = join(HERE, "shots");
|
||||
const BASE = "http://127.0.0.1:7331";
|
||||
|
||||
function hasAgentBrowser() {
|
||||
const probe = spawnSync("agent-browser", ["--version"], { encoding: "utf8", shell: true });
|
||||
return probe.status === 0;
|
||||
}
|
||||
|
||||
function ab(args) {
|
||||
const r = spawnSync("agent-browser", args, { encoding: "utf8", shell: true });
|
||||
if (r.status !== 0) {
|
||||
console.error(`agent-browser ${args.join(" ")} failed:\n${r.stderr || r.stdout}`);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function serverUp() {
|
||||
// Node 18+ has global fetch. Confirm the preview server is reachable.
|
||||
return fetch(`${BASE}/api/state`).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!hasAgentBrowser()) {
|
||||
console.log("agent-browser is not installed -> skipping screenshots.");
|
||||
console.log("Install it with: npm i -g agent-browser && agent-browser install");
|
||||
console.log("Then re-run: node extensions/connector-namespaces/preview/shots.mjs");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!(await serverUp())) {
|
||||
console.error("preview server is not reachable at " + BASE);
|
||||
console.error("start it first: node extensions/connector-namespaces/preview/server.mjs");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
mkdirSync(SHOTS, { recursive: true });
|
||||
|
||||
// Static states.
|
||||
const states = [
|
||||
["catalog", `${BASE}/`],
|
||||
["catalog-restart-banner", `${BASE}/?restart=1`],
|
||||
["catalog-installed", `${BASE}/?installed=1`],
|
||||
["setup", `${BASE}/setup`],
|
||||
["error", `${BASE}/error`],
|
||||
];
|
||||
for (const [name, target] of states) {
|
||||
ab(["open", target]);
|
||||
ab(["screenshot", join(SHOTS, `${name}.png`)]);
|
||||
console.log(`captured ${name}`);
|
||||
}
|
||||
|
||||
// Flow 1: connect -> connecting spinner. The preview /api/install returns
|
||||
// needsConsent and /oauth-status stays pending, so the .si-spin modal
|
||||
// spinner keeps animating. Best-effort selector; adjust if markup changes.
|
||||
ab(["open", `${BASE}/`]);
|
||||
ab(["click", ".item-add[data-api]"]);
|
||||
ab(["screenshot", join(SHOTS, "connecting-spinner.png")]);
|
||||
console.log("captured connecting-spinner (verify the spinner is mid-rotation)");
|
||||
|
||||
// Flow 2: banner -> dismiss -> gone. Screenshot before and after the click
|
||||
// so a frozen/broken dismiss button is visible as a diff.
|
||||
ab(["open", `${BASE}/?restart=1`]);
|
||||
ab(["screenshot", join(SHOTS, "banner-before-dismiss.png")]);
|
||||
ab(["click", ".restart-banner .rb-dismiss"]);
|
||||
ab(["screenshot", join(SHOTS, "banner-after-dismiss.png")]);
|
||||
console.log("captured banner-before-dismiss / banner-after-dismiss (after should have no banner)");
|
||||
|
||||
console.log(`\nshots written to ${SHOTS}`);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user