Answer stability round 1: 1 🔴, 3 🟡 (0 misread); scope the visual selector, check label and cover the gaps
This commit is contained in:
@@ -312,7 +312,7 @@ test("the reference plugin: public Overview is open to all, My shifts takes any
|
||||
await expect(page.locator('.sidebar a[href="/dashboard"]')).toHaveCount(1); // the one unified menu renders
|
||||
await expect(page.locator('.sidebar a[href="/scheduling"]')).toHaveCount(1); // public Overview shown
|
||||
await expect(page.locator('.sidebar a[href="/scheduling/shifts"]')).toHaveCount(0); // gated leaf filtered out
|
||||
await expect(page.locator('.sidebar a[href="/scheduling/mine"]')).toHaveCount(1); // session gate: a session is enough
|
||||
await expect(page.locator('.sidebar nav a[href="/scheduling/mine"]')).toHaveCount(1); // session gate: a session is enough
|
||||
|
||||
// No shifts upstream on this stack, so this also pins the degraded page: the reason, never a 500
|
||||
// and never a claim about what is assigned.
|
||||
|
||||
@@ -77,6 +77,10 @@ const badCases: Array<{ name: string; files: Record<string, string>; match: RegE
|
||||
{ name: "a profile-menu entry naming no gate", files: { "nogateprofile/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", profileMenu: [{ href: "/x", label: "X" }] };` }, match: /nogateprofile.*profile-menu entry "X".*names no gate/s },
|
||||
{ name: "a profile-menu entry gating on a bare word", files: { "bareprofile/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", profileMenu: [{ href: "/x", label: "X", permission: "admin" }] };` }, match: /bareprofile.*admin.*<resource>:<action>/s },
|
||||
{ name: "a profile-menu entry without an href", files: { "nohrefprofile/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", profileMenu: [{ label: "X", session: true }] };` }, match: /nohrefprofile.*profile-menu entry "X".*href/s },
|
||||
{ name: "a profile-menu entry with an empty href", files: { "emptyhref/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", profileMenu: [{ href: "", label: "X", session: true }] };` }, match: /emptyhref.*needs an href/s },
|
||||
{ name: "a profile-menu entry without a label", files: { "nolabel/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", profileMenu: [{ href: "/x", session: true }] };` }, match: /nolabel.*needs a label/s },
|
||||
{ name: "an ungated profile-menu entry after a valid one", files: { "secondbad/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", profileMenu: [{ href: "/a", label: "A", session: true }, { href: "/b", label: "B" }] };` }, match: /secondbad.*"B".*names no gate/s },
|
||||
{ name: "a null profile-menu entry", files: { "nullentry/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", profileMenu: [null] };` }, match: /nullentry.*needs an href/s },
|
||||
{ name: "profileMenu that is not an array", files: { "objprofile/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", profileMenu: {} };` }, match: /objprofile.*"profileMenu" must be an array/s },
|
||||
{ name: "a nav node gating on a bare word", files: { "barenav/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", nav: [{ id: "n", label: "N", permission: "admin" }] };` }, match: /barenav.*admin.*<resource>:<action>/s },
|
||||
{ name: "a declared permission that is a bare word", files: { "baredecl/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", permissions: [{ name: "admin" }] };` }, match: /baredecl.*admin.*<resource>:<action>/s },
|
||||
|
||||
@@ -195,6 +195,7 @@ function findProfileMenuError(entries: PluginManifest["profileMenu"]): string |
|
||||
for (const entry of Array.isArray(entries) ? entries : []) {
|
||||
const what = `profile-menu entry "${entry?.label ?? "?"}"`;
|
||||
if (typeof entry?.href !== "string" || entry.href === "") return `${what} needs an href`;
|
||||
if (typeof entry.label !== "string" || entry.label === "") return `${what} needs a label`;
|
||||
const err = gateError(what, entry);
|
||||
if (err) return err;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export type { RequestContext, User } from "../http/context.ts";
|
||||
export type { PageChrome, ProfileMenuEntry } from "../ui/chrome.ts";
|
||||
export type { NavNode } from "../ui/nav.ts";
|
||||
export { can, check, GuardError, requireSession } from "../auth/guards.ts";
|
||||
// The three coarse gates a route or nav node may declare — `Route` and `NavNode` both extend it.
|
||||
// The three coarse gates a route, nav node or profile-menu entry may declare — `Route`, `NavNode` and `ProfileMenuEntry` all extend it.
|
||||
export type { Gate } from "../auth/gate.ts";
|
||||
// Translation: `ctx.t` and the view-level `t(...)` do the work at runtime — these are for
|
||||
// authoring a plugin's own catalogs (plugins/<id>/i18n/<locale>.ts) and for building a translator
|
||||
|
||||
@@ -138,7 +138,7 @@ test("findConflicts: a route, nav node or profile entry gating on a permission n
|
||||
{ handler: noopHandler, method: "GET", path: "/y", permission: "both:read" },
|
||||
{ handler: noopHandler, method: "POST", path: "/y", permission: "both:read" },
|
||||
{ handler: noopHandler, method: "GET", path: "/z", permission: "declared:write" },
|
||||
], permissions: [], profileMenu: [{ href: "/p", label: "P", permission: "profileonly:read" }] }),
|
||||
], permissions: [], profileMenu: [{ href: "/p", label: "P", permission: "profileonly:read" }, { href: "/q", label: "Q", session: true }, { href: "/r", label: "R", public: true }] }),
|
||||
p({ id: "b", nav: [{ id: "b:y", label: "Y", permission: "both:read" }], permissions: [{ name: "declared:write" }] }),
|
||||
]);
|
||||
assert.ok(gated.every((c) => c.kind === "permission" && c.level === "warn"));
|
||||
|
||||
@@ -96,18 +96,18 @@ const account: Plugin = {
|
||||
{ href: "/account/help", label: "Help", public: true },
|
||||
],
|
||||
};
|
||||
const entries = (opts: { permissions?: string[]; signedIn?: boolean }): { href: string; label: string }[] =>
|
||||
const entries = (opts: { permissions?: string[]; signedIn?: boolean }): { href: string; icon?: string; label: string }[] =>
|
||||
buildPluginChrome({
|
||||
localeHref: (href) => `${href}?locale=sv-SE`,
|
||||
menu: DEFAULT_MENU,
|
||||
plugins: [account],
|
||||
translatorFor: () => (key) => (key === "account.settings" ? "Inställningar" : key),
|
||||
...(opts.signedIn === false ? {} : { user: { email: "ada@x.io", id: "u1", permissions: opts.permissions ?? [] } }),
|
||||
}).user.menu!.map(({ href, label }) => ({ href, label }));
|
||||
}).user.menu!;
|
||||
|
||||
test("profile menu entries are gated like nav nodes, translated by their plugin and carry the locale", () => {
|
||||
assert.deepEqual(entries({}), [
|
||||
{ href: "/account/settings?locale=sv-SE", label: "Inställningar" },
|
||||
{ href: "/account/settings?locale=sv-SE", icon: "i-user", label: "Inställningar" },
|
||||
{ href: "/account/help?locale=sv-SE", label: "Help" },
|
||||
]);
|
||||
assert.deepEqual(entries({ permissions: ["billing:read"] }).map((e) => e.label), ["Inställningar", "account.billing", "Help"]);
|
||||
|
||||
Reference in New Issue
Block a user