import json import re import unittest from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] CRM_FRONTEND = REPO_ROOT / "salesforce-crm-standard" / "frontend" ROUTE_POLICY = CRM_FRONTEND / "src" / "route-surface-policy.ts" def read_text(path: Path) -> str: return path.read_text(encoding="utf-8") def load_json(path: Path): with path.open(encoding="utf-8") as handle: return json.load(handle) def ts_string_array(source: str, name: str) -> list[str]: match = re.search( rf"export const {re.escape(name)} = \[(.*?)\] as const", source, flags=re.DOTALL, ) if not match: raise AssertionError(f"missing array export {name}") return re.findall(r"'([^']*)'", match.group(1)) def ts_string_const(source: str, name: str) -> str: match = re.search(rf"export const {re.escape(name)}(?:[^=]*) = '([^']*)';", source) if not match: raise AssertionError(f"missing string export {name}") return match.group(1) def parse_route_entries(source: str) -> list[dict[str, object]]: match = re.search( r"export const CRM_FRONTEND_ROUTES = \[(.*?)\] as const", source, flags=re.DOTALL, ) if not match: raise AssertionError("missing CRM_FRONTEND_ROUTES export") routes: list[dict[str, object]] = [] for line in match.group(1).splitlines(): stripped = line.strip().rstrip(",") if not stripped.startswith("{ id:"): continue route: dict[str, object] = {} for key in ( "id", "path", "slot", "componentKey", "requiredCapability", "redirectTo", ): value = re.search(rf"{key}: '([^']*)'", stripped) if value: route[key] = value.group(1) order = re.search(r"order: ([0-9]+)", stripped) if not order: raise AssertionError(f"missing order in route line: {stripped}") route["order"] = int(order.group(1)) feature_boundary = re.search(r"featureBoundary: (true|false)", stripped) if feature_boundary: route["featureBoundary"] = feature_boundary.group(1) == "true" routes.append(route) return routes def parse_simple_object_entries(source: str, name: str) -> list[dict[str, str]]: match = re.search( rf"export const {re.escape(name)} = \[(.*?)\] as const", source, flags=re.DOTALL, ) if not match: raise AssertionError(f"missing object array export {name}") entries: list[dict[str, str]] = [] for line in match.group(1).splitlines(): stripped = line.strip().rstrip(",") if not stripped.startswith("{"): continue entry = { key: value for key, value in re.findall(r"([A-Za-z0-9_]+): '([^']*)'", stripped) } if entry: entries.append(entry) return entries class CrmFrontendRouteSurfacePolicyTest(unittest.TestCase): def test_package_exports_and_manifest_entrypoints_include_route_surface_source(self): package_json = load_json(CRM_FRONTEND / "package.json") manifest = load_json(CRM_FRONTEND / "source-manifest.json") self.assertEqual( package_json["exports"]["./route-surface-policy"], "./src/route-surface-policy.ts", ) self.assertIn( "src/route-surface-policy.ts", manifest["packageBuild"]["entrypoints"], ) def test_route_policy_preserves_current_crm_host_app_identity(self): source = read_text(ROUTE_POLICY) self.assertEqual(ts_string_const(source, "CRM_FRONTEND_APP_ID"), "crm") self.assertEqual(ts_string_const(source, "CRM_FRONTEND_APP_LABEL"), "CRM") self.assertEqual(ts_string_const(source, "CRM_SETUP_SURFACE_ROUTE_ID"), "crm.setup") self.assertEqual( ts_string_array(source, "CRM_FRONTEND_ROUTE_COMPONENT_KEYS"), [ "appStudio", "visualFlowBuilder", "appHomeRedirect", "dashboard", "dashboardRedirect", "reportBuilder", "reportsRedirect", "searchResults", "objectView", "setup", "recycleBin", "chatter", "approvalQueue", "aiIntegrationHelp", "apiTokens", "connections", ], ) self.assertEqual( parse_route_entries(source), [ {"id": "crm.studio", "path": "/studio/:appId", "slot": "standalone", "componentKey": "appStudio", "featureBoundary": True, "order": 10}, {"id": "crm.flow-builder", "path": "/flows/builder/:id", "slot": "standalone", "componentKey": "visualFlowBuilder", "requiredCapability": "ViewSetup", "featureBoundary": True, "order": 20}, {"id": "crm.approval-builder", "path": "/approval-processes/builder/:id", "slot": "standalone", "componentKey": "visualFlowBuilder", "requiredCapability": "ViewSetup", "featureBoundary": True, "order": 30}, {"id": "crm.home", "path": "/", "slot": "shell", "componentKey": "appHomeRedirect", "order": 10}, {"id": "crm.dashboards", "path": "/dashboards", "slot": "shell", "componentKey": "dashboard", "order": 20}, {"id": "crm.dashboard-redirect", "path": "/dashboard", "slot": "shell", "componentKey": "dashboardRedirect", "redirectTo": "/dashboards", "order": 30}, {"id": "crm.dashboard-detail", "path": "/dashboard/:dashboardId", "slot": "shell", "componentKey": "dashboard", "order": 40}, {"id": "crm.report-builder", "path": "/analytics/report-builder", "slot": "shell", "componentKey": "reportBuilder", "featureBoundary": True, "order": 50}, {"id": "crm.analytics-redirect", "path": "/analytics", "slot": "shell", "componentKey": "dashboardRedirect", "redirectTo": "/dashboards", "order": 60}, {"id": "crm.reports-redirect", "path": "/reports", "slot": "shell", "componentKey": "reportsRedirect", "redirectTo": "/object/Report", "order": 70}, {"id": "crm.search", "path": "/search", "slot": "shell", "componentKey": "searchResults", "order": 80}, {"id": "crm.object-list", "path": "/object/:objectApiName", "slot": "shell", "componentKey": "objectView", "order": 90}, {"id": "crm.object-record", "path": "/object/:objectApiName/:recordId", "slot": "shell", "componentKey": "objectView", "order": 100}, {"id": "crm.lightning-object-list", "path": "/lightning/o/:objectApiName/list", "slot": "shell", "componentKey": "objectView", "order": 110}, {"id": "crm.lightning-object-view", "path": "/lightning/o/:objectApiName/view", "slot": "shell", "componentKey": "objectView", "order": 120}, {"id": "crm.lightning-record-view", "path": "/lightning/r/:objectApiName/:recordId/view", "slot": "shell", "componentKey": "objectView", "order": 130}, {"id": "crm.setup", "path": "/setup/*", "slot": "shell", "componentKey": "setup", "requiredCapability": "ViewSetup", "order": 140}, {"id": "crm.recycle-bin", "path": "/recyclebin", "slot": "shell", "componentKey": "recycleBin", "requiredCapability": "ViewSetup", "order": 150}, {"id": "crm.chatter", "path": "/chatter", "slot": "shell", "componentKey": "chatter", "order": 160}, {"id": "crm.approvals", "path": "/approvals", "slot": "shell", "componentKey": "approvalQueue", "order": 170}, {"id": "crm.ai-integration-help", "path": "/help/ai-integration", "slot": "shell", "componentKey": "aiIntegrationHelp", "order": 180}, {"id": "crm.api-tokens", "path": "/settings/api-tokens", "slot": "shell", "componentKey": "apiTokens", "order": 190}, {"id": "crm.connections", "path": "/settings/connections", "slot": "shell", "componentKey": "connections", "order": 200}, ], ) self.assertIn( {"id": "crm.setup", "path": "/setup/*", "slot": "shell", "componentKey": "setup", "requiredCapability": "ViewSetup", "order": 140}, parse_route_entries(source), ) def test_route_policy_preserves_standard_tabs_and_navigation_capabilities(self): source = read_text(ROUTE_POLICY) self.assertEqual( parse_simple_object_entries(source, "CRM_STANDARD_PAGE_TABS"), [ {"key": "Home", "routeId": "crm.home", "label": "Home", "icon": "Home"}, {"key": "Chatter", "routeId": "crm.chatter", "label": "Chatter", "icon": "MessageCircle"}, {"key": "WaveHomeLightning", "routeId": "crm.dashboards", "label": "Dashboards", "icon": "BarChart3"}, ], ) self.assertEqual( parse_simple_object_entries(source, "CRM_APP_NAVIGATION_CAPABILITIES"), [ {"capability": "approvals", "routeId": "crm.approvals"}, {"capability": "dashboardDetail", "routeId": "crm.dashboard-detail"}, {"capability": "dashboardList", "routeId": "crm.dashboards"}, {"capability": "flowBuilder", "routeId": "crm.flow-builder"}, {"capability": "recycleBin", "routeId": "crm.recycle-bin"}, {"capability": "studio", "routeId": "crm.studio"}, ], ) if __name__ == "__main__": unittest.main()