Files
govoplan-core/webui/scripts/test-workspace-collection-actions.mjs

84 lines
5.1 KiB
JavaScript

import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
import ts from "typescript";
// Collection-wide commands belong to the full workspace, never its left pane.
// Keep editor/detail commands scoped to their own pane. The real-page browser
// scenarios separately verify the shared action engine's physical placement.
const pages = [
["workflow", "workflow/WorkflowPage.tsx"],
["dataflow", "dataflow/DataflowPage.tsx"],
["templates", "templates/TemplatesPage.tsx"],
["datasources", "datasources/DatasourcesPage.tsx"],
["dist-lists", "distributionLists/DistributionListsPage.tsx"],
["tasks", "tasks/TasksPage.tsx"],
["voting", "voting/VotingPage.tsx"],
["scheduling", "scheduling/SchedulingPage.tsx"],
["committee", "committee/CommitteePage.tsx"]
];
function attribute(node, name) {
const item = node.attributes.properties.find(prop => ts.isJsxAttribute(prop) && prop.name.getText() === name);
return item?.initializer && ts.isStringLiteral(item.initializer) ? item.initializer.text : item;
}
let checked = 0;
for (const [module, page] of pages) {
const path = resolve(import.meta.dirname, `../../../govoplan-${module}/webui/src/features/${page}`);
// Core-only checkouts must not acquire dependencies on optional modules.
if (!existsSync(path)) continue;
checked += 1;
const source = ts.createSourceFile(path, readFileSync(path, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const bars = [];
function visit(node) {
if (ts.isJsxSelfClosingElement(node) && node.tagName.getText() === "WorkspaceActionBar" && attribute(node, "variant") === "collection") bars.push(node);
ts.forEachChild(node, visit);
}
visit(source);
assert.equal(bars.length, 1, `${module}: one collection-wide action bar`);
const bar = bars[0];
assert.equal(attribute(bar, "scope"), "workspace", `${module}: collection commands use workspace scope`);
assert.ok(attribute(bar, "refreshable") && attribute(bar, "reloadAction") && attribute(bar, "createAction"), `${module}: Reload and creation stay in the same semantic bar`);
let parent = bar.parent;
while (parent && !ts.isSourceFile(parent)) {
assert.ok(!(ts.isJsxAttribute(parent) && parent.name.getText() === "primary"), `${module}: collection commands must not be nested in the left pane`);
parent = parent.parent;
}
let enclosingFrame = bar.parent;
while (enclosingFrame && !(ts.isJsxElement(enclosingFrame) && enclosingFrame.openingElement.tagName.getText() === "WorkspaceFrame")) enclosingFrame = enclosingFrame.parent;
assert.ok(enclosingFrame, `${module}: the persistent workspace owns its commands`);
if (module !== "committee" && module !== "scheduling") {
const layout = enclosingFrame.children.find(node => ts.isJsxElement(node) && node.openingElement.tagName.getText() === "WorkspaceLayout");
assert.ok(layout && bar.pos < layout.pos, `${module}: commands precede the split workspace`);
}
}
// Explorer modules use custom virtualized panes, but not a custom page toolbar.
for (const [module, page, variant] of [["files", "files/FilesPage.tsx", "collection"], ["mail", "mail/MailboxPage.tsx", "workspace"]]) {
const path = resolve(import.meta.dirname, `../../../govoplan-${module}/webui/src/features/${page}`);
if (!existsSync(path)) continue;
checked += 1;
const source = ts.createSourceFile(path, readFileSync(path, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
const nodes = [];
function visit(node) { nodes.push(node); ts.forEachChild(node, visit); }
visit(source);
const bars = nodes.filter(node => ts.isJsxSelfClosingElement(node) && node.tagName.getText() === "WorkspaceActionBar" && attribute(node, "scope") === "workspace");
assert.equal(bars.length, 1, `${module}: one persistent explorer-wide action bar`);
const bar = bars[0];
assert.equal(attribute(bar, "variant"), variant, `${module}: explicit semantic action variant`);
assert.ok(attribute(bar, "refreshable") && attribute(bar, "reloadAction"), `${module}: current-view Reload is semantic`);
assert.equal(Boolean(attribute(bar, "createAction")), module === "files", `${module}: creation only where an owning workflow exists`);
const owner = bar.parent;
const placements = ts.isVariableDeclaration(owner)
? nodes.filter(node => ts.isJsxExpression(node) && node.expression && ts.isIdentifier(node.expression) && node.expression.text === owner.name.getText())
: [bar];
assert.equal(placements.length, 1, `${module}: workspace bar has one persistent placement`);
const placement = placements[0];
assert.ok(ts.isJsxElement(placement.parent) && placement.parent.openingElement.tagName.getText() === "WorkspaceFrame", `${module}: commands are direct workspace children, outside panes and selection branches`);
const shell = placement.parent.children.find(node => ts.isJsxElement(node) && node.openingElement.tagName.getText() === "div" && /file-manager-shell/.test(node.openingElement.getText()));
assert.ok(shell && placement.pos < shell.pos, `${module}: toolbar precedes the explorer panes`);
}
console.log(`Workspace actions: ${checked} present module pages conform.`);