toga-ai 1.0.768 → 1.0.770
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/_underscore/features/acl-permission-chain.md +13 -1
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +3 -3
- package/knowledge/2.0/apps/toga25-supply/architecture.md +13 -0
- package/knowledge/2.0/apps/toga25-supply/features/client-configurable-fields.md +39 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +42 -0
- package/knowledge/2.0/apps/toga25-supply/workflows/cypress-testing.md +256 -119
- package/knowledge/2.0/standards/frontend.md +103 -10
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-03
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "tcox", "bala", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -380,6 +380,14 @@ across clients, establish **which app** the missing control is in. Detail:
|
|
|
380
380
|
[Surface Resolver](surface-resolver.md) and
|
|
381
381
|
[toga25-supply side navigation](../../toga25-supply/features/side-navigation-and-default-route.md).
|
|
382
382
|
|
|
383
|
+
> **Narrow scope precisely: the FLAGS are inert, the ENDPOINT is not gone.** "Dead config" above is
|
|
384
|
+
> about the `navigation-*` `AclActions` flags having no effect on the 2.5 UI — that stands, verified
|
|
385
|
+
> prod-wide. It is **not** a claim that `toga25-supply` has stopped calling `Page::meta`: verified
|
|
386
|
+
> 2026-09-03, five of its view models still call `GET /v2/pages/meta` (nothing in `src/` reads a
|
|
387
|
+
> field off the result). So a change to `Page::meta`'s request path can still reach the 2.5 app even
|
|
388
|
+
> though its ACL flags cannot reach the 2.5 nav. Detail:
|
|
389
|
+
> [Surface Frontend](../../toga25-supply/features/surface-frontend.md).
|
|
390
|
+
|
|
383
391
|
Worked example (Quad, 2026-08-26): the vendor-items **Add and Edit buttons both** read
|
|
384
392
|
`acl['vendor-items']['navigation-edit-on-vendor-items']` (AclActions id 7). `Client_Quad` had a row
|
|
385
393
|
for id 5 only, so the page was reachable but button-less, even though record 19 already granted role
|
|
@@ -598,6 +606,10 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
|
|
|
598
606
|
and every repo is on the **same branch** so the generated model matches the DB.
|
|
599
607
|
|
|
600
608
|
## Change history
|
|
609
|
+
- **2026-09-03** — Scoped the "dead config in the surface-driven 2.5 app" claim: the `navigation-*`
|
|
610
|
+
**flags** are inert (unchanged), but `toga25-supply` still **calls `GET /v2/pages/meta`** from five
|
|
611
|
+
view models, so `Page::meta` is not out of that app's request path. Additive clarification only.
|
|
612
|
+
(apeterson)
|
|
601
613
|
- **2026-09-01** — Four additions from a `PurchaseOrderItems._qty*` grant investigation.
|
|
602
614
|
(1) **Read `Core.Records.aclDatabase` FIRST** — both databases carry an `AclFieldPermissions`
|
|
603
615
|
table, so checking Core for a `CLIENT`-acl record (here `purchase-order-items`, record 18) returns
|
|
@@ -5,16 +5,16 @@
|
|
|
5
5
|
| [TOGa 2.5 Supply — Architecture](architecture.md) | `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply application — an iteration and improvement of `toga2-supply`. | toga25-supply/src/main.tsx, toga25-supply/src/App.tsx, toga25-supply/src/routes.tsx, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/layout/, toga25-supply/src/pages/, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/providers/TalosStreamProvider.tsx |
|
|
6
6
|
| [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | A declarative, fully config-driven rule engine that resolves the boolean-ish flags (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. | toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.test.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalFlowDetailInputs.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/FIELDS/COMPASS/approvalActionFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/FIELDS/COMPASSCANADA/approvalActionFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
7
7
|
| [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | The mechanism that resolves a client's **API-fetch projection** — which `fields` / `ojoin` / `join` / `where` to request from the TOGa API for a given page — fr | toga25-supply/src/fieldsConfig/resolveApiConfig.ts, toga25-supply/src/fieldsConfig/useClientApiFields.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx |
|
|
8
|
-
| [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
|
|
8
|
+
| [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/resolveRole.ts, toga25-supply/src/globalFieldsConfig/clientLoginFields.json, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
|
|
9
9
|
| [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql |
|
|
10
10
|
| [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
|
|
11
11
|
| [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
|
|
12
12
|
| [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css |
|
|
13
13
|
| [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. | toga25-supply/src/routes.tsx, toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts, api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
14
14
|
| [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
|
|
15
|
-
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
15
|
+
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx, toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
|
|
16
16
|
| [Talos Integration (AppLayout host, live Aegra streaming, LangGraph→blox mapper)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/api/talos.ts, toga25-supply/src/hooks/useTalosSession.ts, toga25-supply/src/hooks/useTalosThreads.ts, toga25-supply/src/hooks/useTalosThreads.test.ts, toga25-supply/src/hooks/useTalosSurface.ts, toga25-supply/src/hooks/useTalosSurface.test.ts, toga25-supply/src/App.tsx, toga25-supply/db-migrations/PLAYBOOK.md, dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/src/utils/talosMessages.ts, toga25-supply/src/utils/talosMessages.test.ts, toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
|
|
17
17
|
| [Transfer Orders page (TableView → Core surfaces → React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen — list + read-only record modal — built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/PanelSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/ContactSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/SelectTargetLocationModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferItemPickerTable.tsx, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderNote.php, _underscore/Model/Client/TransferOrderNoteType.php, dbchanges2/Client/2026-09-01b - TransferOrderNotesTables.sql, dbchanges2/Client/2026-09-01c - TransferOrderContactCreatedByAndDescription.sql, dbchanges2/Core/2026-09-01c - TransferOrderNotesRecords.sql, dbchanges2/Core/2026-09-01d - TransferOrderContactCreatedByAndDescriptionRecordFields.sql, toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/pages/TransferOrders/TransferOrder.module.css, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql |
|
|
18
18
|
| [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json, toga25-supply/.env.development |
|
|
19
|
-
| [Cypress Testing Harness (
|
|
19
|
+
| [Cypress Testing Harness (multi-tenant, fully stubbed)](workflows/cypress-testing.md) | The Cypress harness for `toga25-supply`: an **e2e** project and a **component** project, plus a stub layer that answers **every** `/v2` call so no test touches | toga25-supply/cypress.config.ts, toga25-supply/cypress/README.md, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/tenants.ts, toga25-supply/cypress/support/api/stubApi.ts, toga25-supply/cypress/support/api/envelope.ts, toga25-supply/cypress/support/api/users.ts, toga25-supply/cypress/support/api/surfaces.ts, toga25-supply/cypress/support/api/session.ts, toga25-supply/cypress/e2e/login.cy.ts, toga25-supply/cypress/e2e/emptyNavigation.cy.ts, toga25-supply/cypress/component/harness.cy.tsx, toga25-supply/cypress/fixtures/tenants/COMPASS/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/COMPASSCANADA/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/NYCHH/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/QUAD/surfaces/navigation.json, toga25-supply/scripts/cypress.mjs, toga25-supply/tsconfig.node.json, toga25-supply/package.json |
|
|
20
20
|
| [Porting a page (or query) from toga2-supply to toga25-supply](workflows/porting-a-page-from-toga2-supply.md) | `toga25-supply` re-implements pages that already exist in `toga2-supply`. | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/components/ui/Tables/hooks/useExportableData.tsx |
|
|
@@ -19,6 +19,7 @@ files:
|
|
|
19
19
|
- toga25-supply/src/hooks/useTableCellInteractions.ts
|
|
20
20
|
- toga25-supply/src/providers/TalosStreamProvider.tsx
|
|
21
21
|
related:
|
|
22
|
+
- workflows/cypress-testing.md
|
|
22
23
|
- features/talos-integration.md
|
|
23
24
|
- features/client-configurable-fields.md
|
|
24
25
|
- features/meta-driven-table-data.md
|
|
@@ -70,6 +71,18 @@ toga-blox table/modal templates.
|
|
|
70
71
|
- **UI library:** `@agilant/toga-blox` provides `PrimaryTable*Layout`, `TableRecordModal`,
|
|
71
72
|
`BaseInput`, `BaseButton`, `DetailSection`, the `useTableData` hook, FontAwesome helpers, etc.
|
|
72
73
|
|
|
74
|
+
### Testing
|
|
75
|
+
|
|
76
|
+
Two harnesses, both fully stubbed — **no front-end test ever talks to a backend**:
|
|
77
|
+
|
|
78
|
+
- **Vitest + React Testing Library** (`npm test`) for logic: hooks, view models, auth context,
|
|
79
|
+
table URL-state, error classification.
|
|
80
|
+
- **Cypress** (`npm run cy:e2e:<tenant>`, `npm run cy:ct`) for the **tenant x role x feature**
|
|
81
|
+
matrix, driven by per-tenant surface **fixtures** rather than real logins or a seeded database.
|
|
82
|
+
|
|
83
|
+
The full design, run commands, guardrails and traps live in
|
|
84
|
+
[cypress-testing](workflows/cypress-testing.md) — read that before writing or changing a test.
|
|
85
|
+
|
|
73
86
|
## Directory layout (`src/`)
|
|
74
87
|
|
|
75
88
|
- `pages/<Entity>/` — page component (thin) + `viewModel/` (the `use*PageViewModel` hook) +
|
|
@@ -6,10 +6,12 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-03
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/fieldsConfig/index.ts
|
|
13
|
+
- toga25-supply/src/fieldsConfig/resolveRole.ts
|
|
14
|
+
- toga25-supply/src/globalFieldsConfig/clientLoginFields.json
|
|
13
15
|
- toga25-supply/src/fieldsConfig/useClientFields.ts
|
|
14
16
|
- toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/
|
|
15
17
|
- toga25-supply/src/pages/Inventory/viewModel/FIELDS/
|
|
@@ -61,6 +63,37 @@ and the `useClientFields()` hook.
|
|
|
61
63
|
const tenantFields = clientFields.salesOrdersPageFields; // your key
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
### How `resolveRole` decides the role (verified 2026-09-03)
|
|
67
|
+
|
|
68
|
+
`src/fieldsConfig/resolveRole.ts` takes `(clientSlug, user)` and reads **boolean-ish string flags
|
|
69
|
+
off the `/v2/users` record** — `"1"` means set. The flag set and the returned role names **differ
|
|
70
|
+
per client**, and the checks are ordered, so the first match wins:
|
|
71
|
+
|
|
72
|
+
| clientSlug | flag (in order) | role returned |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `COMPASS`, `COMPASSCANADA` | `_isAdmin` | `ADMIN` |
|
|
75
|
+
| | `_isSuperAdmin` | `SUPERUSER` |
|
|
76
|
+
| | `_isSupervisor` | `MANAGER` |
|
|
77
|
+
| | none set | `USER` |
|
|
78
|
+
| `QUAD` | `_isGlobalAdmin` | `GLOBALADMIN` |
|
|
79
|
+
| | `_isBuyer` | `BUYER` |
|
|
80
|
+
| | `_isItShopper` | `ITSHOPPER` |
|
|
81
|
+
| | none set | `USER` |
|
|
82
|
+
| any other client | — | `DEFAULT` |
|
|
83
|
+
|
|
84
|
+
Two things follow that catch people out:
|
|
85
|
+
|
|
86
|
+
- **Which flags are even requested is per-client**, declared in
|
|
87
|
+
`src/globalFieldsConfig/clientLoginFields.json` (`userApiFields`). A flag the client does not
|
|
88
|
+
request is never fetched, so `resolveRole` cannot see it. `NYCHH`, for example, has no entry, so
|
|
89
|
+
no role flags are requested and the role is always `DEFAULT`.
|
|
90
|
+
- **An ungranted or unrequested field comes back ABSENT, not null**, from the API (see the
|
|
91
|
+
[ACL permission chain](../../_underscore/features/acl-permission-chain.md)) — so `user._isAdmin`
|
|
92
|
+
being `undefined` is the normal shape, not a bug.
|
|
93
|
+
|
|
94
|
+
The Cypress harness mirrors this table as `roleFlags` per tenant in `cypress/support/tenants.ts`;
|
|
95
|
+
keep the two in step. See [Cypress testing harness](../workflows/cypress-testing.md).
|
|
96
|
+
|
|
64
97
|
### By client × role vs. by client only
|
|
65
98
|
|
|
66
99
|
- **Client × role** (e.g. `salesOrdersPageFields` — ADMIN vs MANAGER see different filter buttons):
|
|
@@ -168,6 +201,11 @@ modal's chrome from the same per-client JSON, not from the component:
|
|
|
168
201
|
`vendorItemRecordViewFields` are record-modal-layout examples living under `src/layout/.../viewModel/FIELDS/`.
|
|
169
202
|
|
|
170
203
|
## Change history
|
|
204
|
+
- 2026-09-03 — Documented the actual `resolveRole` flag → role table (COMPASS/COMPASSCANADA:
|
|
205
|
+
`_isAdmin`/`_isSuperAdmin`/`_isSupervisor`, in that order, else `USER`; QUAD:
|
|
206
|
+
`_isGlobalAdmin`/`_isBuyer`/`_isItShopper`, else `USER`; every other client `DEFAULT`) and the
|
|
207
|
+
fact that which flags are requested is itself per-client, via
|
|
208
|
+
`globalFieldsConfig/clientLoginFields.json` — so NYCHH is always `DEFAULT`. (apeterson)
|
|
171
209
|
- 2026-07-20 — Extracted the **API-fetch projection** out of these presentation JSON blobs into a
|
|
172
210
|
new per-page `apiFields.json` + `useClientApiFields` mechanism (see
|
|
173
211
|
[client-api-fetch-fields](client-api-fetch-fields.md)). The `salesOrderDetailsConfig` block was
|
|
@@ -28,6 +28,11 @@ files:
|
|
|
28
28
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts
|
|
29
29
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx
|
|
30
30
|
- toga25-supply/src/surface/useFetchSurfaceMeta.ts
|
|
31
|
+
- toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
|
|
32
|
+
- toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx
|
|
33
|
+
- toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
|
|
34
|
+
- toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx
|
|
35
|
+
- toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx
|
|
31
36
|
- toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
|
|
32
37
|
- toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx
|
|
33
38
|
- toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx
|
|
@@ -78,6 +83,7 @@ files:
|
|
|
78
83
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts
|
|
79
84
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx
|
|
80
85
|
related:
|
|
86
|
+
- ../workflows/cypress-testing.md
|
|
81
87
|
- ../../_underscore/features/surface-resolver.md
|
|
82
88
|
- ../../_underscore/features/sales-order-denial-reason.md
|
|
83
89
|
- meta-driven-table-data.md
|
|
@@ -965,7 +971,43 @@ Final: the parameterized FE map above.
|
|
|
965
971
|
button except **View Log** renders and does nothing on click. Whether the SR modal gets its own
|
|
966
972
|
actions surface is **still an open decision**, not an oversight.
|
|
967
973
|
|
|
974
|
+
## ⚠ The app still calls the legacy `GET /v2/pages/meta`
|
|
975
|
+
|
|
976
|
+
Verified 2026-09-03. The Surface layer replaced `Page::meta` **as the source of nav/ACL/label
|
|
977
|
+
config** — that part of the story holds — but the **endpoint is still being called**. Five view
|
|
978
|
+
models call blox's `useFetchPageMeta` → `getPageMeta` → `apiGet("/pages/meta", {page, app})`:
|
|
979
|
+
|
|
980
|
+
- `src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx` (fires **twice** per landing-page load)
|
|
981
|
+
- `src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx`
|
|
982
|
+
- `src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx`
|
|
983
|
+
- `src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx`
|
|
984
|
+
- `src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx`
|
|
985
|
+
|
|
986
|
+
**Nothing in `src/` reads any field off the result.** The only references to `pageMeta.acl` are
|
|
987
|
+
*comments* in `src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts` noting it was replaced. So
|
|
988
|
+
these are almost certainly dead weight left over from the Surface migration — but until they are
|
|
989
|
+
removed, the app still makes the call, and a request-level change to `/pages/meta` can still hit
|
|
990
|
+
this app.
|
|
991
|
+
|
|
992
|
+
Two consequences:
|
|
993
|
+
|
|
994
|
+
- Any test/stub layer must answer `/pages/meta` or the calls fall through (this is how it was
|
|
995
|
+
found — the Cypress fall-through guardrail flagged it on its **first** run, not a code read).
|
|
996
|
+
See [Cypress testing harness](../workflows/cypress-testing.md).
|
|
997
|
+
- **Removing the five calls is separate, unstarted work.** Stubbing them is not the fix.
|
|
998
|
+
|
|
999
|
+
Related: [legacy page meta](../../_underscore/features/page-meta-context-field-settings.md) and the
|
|
1000
|
+
[ACL permission chain](../../_underscore/features/acl-permission-chain.md) (whose "dead config"
|
|
1001
|
+
claim is about the `navigation-*` **flags** being inert — still true — not about the endpoint being
|
|
1002
|
+
unused).
|
|
1003
|
+
|
|
968
1004
|
## Change history
|
|
1005
|
+
- 2026-09-03 — Recorded that the 2.5 app **still calls `GET /v2/pages/meta`** from five view
|
|
1006
|
+
models (SalesOrders — twice per load — VendorItems, Inventory, Bundles, ItemFulfillmentModal)
|
|
1007
|
+
even though nothing in `src/` reads a field off the result. Found by the new Cypress
|
|
1008
|
+
fall-through guardrail. Qualifies, does not overturn, the "Page::meta is replaced" claim: the
|
|
1009
|
+
nav/ACL config really is surface-driven; the endpoint call is leftover. (apeterson)
|
|
1010
|
+
|
|
969
1011
|
- 2026-09-03 — **Built the stacked-Total `+` joiner in the Approval Workflow modal**
|
|
970
1012
|
(`withStackedCurrencyJoiner`, new `RecordApprovalModal/helpers/stackedCurrencyJoiner.ts` + 13
|
|
971
1013
|
vitest cases; one-line wiring in `RecordApprovalModalLayout` `renderField`, workflow modal only —
|
|
@@ -1,143 +1,280 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Cypress Testing Harness (
|
|
2
|
+
title: Cypress Testing Harness (multi-tenant, fully stubbed)
|
|
3
3
|
framework: "2.0"
|
|
4
4
|
repo: toga25-supply
|
|
5
5
|
project: TOGa 2.5 Supply
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-03
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/cypress.config.ts
|
|
13
|
+
- toga25-supply/cypress/README.md
|
|
13
14
|
- toga25-supply/cypress/tsconfig.json
|
|
14
|
-
- toga25-supply/cypress/support/component.tsx
|
|
15
|
-
- toga25-supply/cypress/support/component-index.html
|
|
16
15
|
- toga25-supply/cypress/support/e2e.ts
|
|
17
16
|
- toga25-supply/cypress/support/commands.ts
|
|
18
|
-
- toga25-supply/cypress/support/
|
|
19
|
-
- toga25-supply/cypress/support/
|
|
20
|
-
- toga25-supply/cypress/
|
|
21
|
-
- toga25-supply/cypress/
|
|
22
|
-
- toga25-supply/cypress/
|
|
23
|
-
- toga25-supply/cypress/
|
|
17
|
+
- toga25-supply/cypress/support/component.tsx
|
|
18
|
+
- toga25-supply/cypress/support/component-index.html
|
|
19
|
+
- toga25-supply/cypress/support/tenants.ts
|
|
20
|
+
- toga25-supply/cypress/support/api/stubApi.ts
|
|
21
|
+
- toga25-supply/cypress/support/api/envelope.ts
|
|
22
|
+
- toga25-supply/cypress/support/api/users.ts
|
|
23
|
+
- toga25-supply/cypress/support/api/surfaces.ts
|
|
24
|
+
- toga25-supply/cypress/support/api/session.ts
|
|
25
|
+
- toga25-supply/cypress/e2e/login.cy.ts
|
|
26
|
+
- toga25-supply/cypress/e2e/emptyNavigation.cy.ts
|
|
27
|
+
- toga25-supply/cypress/component/harness.cy.tsx
|
|
28
|
+
- toga25-supply/cypress/fixtures/tenants/COMPASS/surfaces/navigation.json
|
|
29
|
+
- toga25-supply/cypress/fixtures/tenants/COMPASSCANADA/surfaces/navigation.json
|
|
30
|
+
- toga25-supply/cypress/fixtures/tenants/NYCHH/surfaces/navigation.json
|
|
31
|
+
- toga25-supply/cypress/fixtures/tenants/QUAD/surfaces/navigation.json
|
|
32
|
+
- toga25-supply/scripts/cypress.mjs
|
|
33
|
+
- toga25-supply/tsconfig.node.json
|
|
34
|
+
- toga25-supply/package.json
|
|
24
35
|
related:
|
|
25
|
-
- ../features/
|
|
26
|
-
- ../features/
|
|
36
|
+
- ../features/surface-frontend.md
|
|
37
|
+
- ../features/client-configurable-fields.md
|
|
38
|
+
- ../features/side-navigation-and-default-route.md
|
|
39
|
+
- ../features/sso-redirect-and-session-gating.md
|
|
40
|
+
- ../features/force-logout-on-deployment.md
|
|
27
41
|
- ../architecture.md
|
|
42
|
+
- ../../../2.0/standards/frontend.md
|
|
28
43
|
---
|
|
29
44
|
|
|
30
45
|
## What it is
|
|
31
46
|
|
|
32
|
-
The Cypress
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
- **
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
47
|
+
The Cypress harness for `toga25-supply`: an **e2e** project and a **component** project, plus
|
|
48
|
+
a stub layer that answers **every** `/v2` call so no test touches a backend. It exists to test
|
|
49
|
+
the thing that actually breaks in this app — **tenant × role × feature** — without needing real
|
|
50
|
+
logins or a seeded database.
|
|
51
|
+
|
|
52
|
+
Current state (2026-09-03, branch `TRUE-81735`, working tree only): **26 tests green** —
|
|
53
|
+
6 e2e specs × 4 tenants (COMPASS, COMPASSCANADA, NYCHH, QUAD) + 2 component tests.
|
|
54
|
+
`cypress/README.md` is the hands-on version of this doc for a developer working without Claude
|
|
55
|
+
(run commands, recipes, traps). Read that to *write* a test; read this to understand *why* it is
|
|
56
|
+
shaped this way.
|
|
57
|
+
|
|
58
|
+
> **This replaces the earlier harness.** A first harness (approval-modal component specs, a
|
|
59
|
+
> window-backed hook-mock pattern) was built on branch **`TRUE-79813`** (commit `962a41d`
|
|
60
|
+
> "Save") and **never merged** — it is not on `_main`, `_production`, or any other branch, and
|
|
61
|
+
> its mirrored Vite config is now stale (hardcoded `optimizeDeps.exclude` for blox, 3-member
|
|
62
|
+
> `dedupe`). The decision was to **build fresh on `TRUE-81735`** rather than port it. If you go
|
|
63
|
+
> looking for `cy.mountWithProviders` or `cypress/support/mocks/`, that is where it lives — a
|
|
64
|
+
> stranded branch, not the live harness.
|
|
65
|
+
|
|
66
|
+
## The three multi-tenant axes (the core design)
|
|
67
|
+
|
|
68
|
+
The app is multi-tenant along three axes, and **each has a different source of truth**, so each
|
|
69
|
+
is modelled in a different place. Get this and the rest of the harness follows.
|
|
70
|
+
|
|
71
|
+
| Axis | Source of truth in the app | Modelled in the harness as |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| **Client** | `window.location.hostname` — `useHostnameStore` takes the first label, upper-cased | the **`baseUrl`** Cypress visits (`TENANT` env var → `TENANT_HOSTS` in `cypress.config.ts`) |
|
|
74
|
+
| **Role** | flags on the `/users` record, resolved client-side by `src/fieldsConfig/resolveRole.ts` | `roleFlags` per tenant in `cypress/support/tenants.ts` (mirrors that function) |
|
|
75
|
+
| **Features / permissions** | 100% backend — the **Surface** bundles (there is no front-end roster any more) | per-tenant JSON **fixtures** under `cypress/fixtures/tenants/<SLUG>/surfaces/` |
|
|
76
|
+
|
|
77
|
+
**One tenant per run.** Because the client *is* the URL, and Cypress allows one superdomain per
|
|
78
|
+
spec (`compass.togasupply` and `nychh.togasupply` are different superdomains), the runner targets
|
|
79
|
+
a single tenant per invocation: `npm run cy:e2e:compass`, `…:compasscanada`, `…:nychh`, `…:quad`,
|
|
80
|
+
and `cy:e2e:all` chains them. `cypress/support/e2e.ts` has a `before()` that fails the run if
|
|
81
|
+
`Cypress.env("tenant")` and `baseUrl` disagree.
|
|
82
|
+
|
|
83
|
+
**One dev server serves every tenant.** It binds `127.0.0.1` and `vite.config.ts` sets
|
|
84
|
+
`allowedHosts: ['.togasupply']`. So `npm run dev` **once**, then run any tenant. Each tenant host
|
|
85
|
+
needs a `127.0.0.1` line in `/etc/hosts`.
|
|
86
|
+
|
|
87
|
+
**Adding a tenant = 3 edits, no spec changes:** one entry in `TENANTS` (`support/tenants.ts`),
|
|
88
|
+
one fixture folder, and one **static** import + map entry in `support/api/surfaces.ts` (Cypress
|
|
89
|
+
bundles the support file with esbuild, so the imports cannot be dynamic). The specs read the
|
|
90
|
+
registry, so they pick it up.
|
|
91
|
+
|
|
92
|
+
## Derived vs fixtured — the split, and why
|
|
93
|
+
|
|
94
|
+
- **`/users` records are DERIVED.** `support/api/users.ts` builds them from the tenant's
|
|
95
|
+
`roleFlags` **plus the app's own `src/globalFieldsConfig/clientLoginFields.json`**, so a new
|
|
96
|
+
login field there is picked up automatically instead of drifting. Critically, the builder
|
|
97
|
+
returns **only the fields that client requests** — an ungranted or unrequested field comes back
|
|
98
|
+
**ABSENT (not null)** from the real API, so a stub that always returned every flag would hide
|
|
99
|
+
that whole class of bug.
|
|
100
|
+
- **Surface bundles are FIXTURED.** They carry real per-client config that cannot be derived
|
|
101
|
+
from anything on the client — they *are* the thing being simulated. Copy a real
|
|
102
|
+
`GET /v2/surfaces/meta` bundle into the fixture folder and edit it.
|
|
103
|
+
- **A slug with no fixture resolves to an EMPTY bundle** (`emptyBundle()` in
|
|
104
|
+
`support/api/surfaces.ts`) — which is the real "client not seeded / not granted" state and
|
|
105
|
+
exercises the FE's design-literal fallbacks. Leaving a slug unfixtured is the *correct* way to
|
|
106
|
+
test an un-onboarded client (`e2e/emptyNavigation.cy.ts` does exactly this).
|
|
107
|
+
|
|
108
|
+
## The stub layer
|
|
109
|
+
|
|
110
|
+
`cy.stubApi({ tenant?, role?, surfaces?, user?, domainIsValid? })` registers a catch-all plus a
|
|
111
|
+
handler for every endpoint the app calls: `auth/public`, `auth/refresh`, `auth/login`, `users`,
|
|
112
|
+
`surfaces/meta`, `surfaces/meta-group`, `core-parameters`, `pages/meta`, `table-views/meta`,
|
|
113
|
+
`domains`, `client-authentications/singleSignOnServiceUrl`.
|
|
114
|
+
|
|
115
|
+
`support/api/envelope.ts` builds the V2 wrapper (`v2Envelope` / `v2Empty` / `v2Failure`). blox's
|
|
116
|
+
`apiGet`/`apiPost` return `response.data` — i.e. the **whole envelope** — so app code reads
|
|
117
|
+
`envelope.data.<route>`; a stub that puts the payload anywhere else reads back `undefined` and
|
|
118
|
+
the failure looks like a front-end bug.
|
|
119
|
+
|
|
120
|
+
Three custom commands: `cy.stubApi()`, `cy.visitAsGuest(path)` (storage cleared → anonymous),
|
|
121
|
+
`cy.loginAs(role, path)` (seeds a session, lands signed in), plus `cy.getByTestId()` and
|
|
122
|
+
`cy.allowUnstubbedApiCalls()`. Selector convention: prefer `data-testid`, but use an existing
|
|
123
|
+
stable semantic attribute (`input[name]`, `button[type=submit]`, `a[href]`) rather than adding a
|
|
124
|
+
testid for its own sake.
|
|
125
|
+
|
|
126
|
+
## Guardrails built into the suite
|
|
127
|
+
|
|
128
|
+
### The fall-through check (the main one)
|
|
129
|
+
|
|
130
|
+
A catch-all intercept **scoped to the `/v2` path** answers anything not modelled — with an empty
|
|
131
|
+
**success** envelope — and **records the URL**. A global `afterEach` in `support/e2e.ts` fails the
|
|
132
|
+
test if that list is non-empty.
|
|
133
|
+
|
|
134
|
+
- **Why a catch-all at all:** an un-stubbed request would otherwise escape to the real backend —
|
|
135
|
+
slow, non-deterministic, and it can **write real data**.
|
|
136
|
+
- **Why it must be registered FIRST:** Cypress matches intercepts in **reverse order of
|
|
137
|
+
definition**.
|
|
138
|
+
- **Why it is path-scoped:** a bare `**` would also swallow the Vite dev server's own HTML/JS/CSS.
|
|
139
|
+
- **Why the check is worth it:** one check catches **four** bugs — a non-matching intercept
|
|
140
|
+
pattern, a typo'd path, a wrong HTTP method, and an endpoint the app newly calls that nobody
|
|
141
|
+
stubbed. All four are otherwise **completely silent**: the catch-all answers 200 with an empty
|
|
142
|
+
envelope, so the app just behaves as though the record did not exist and the spec goes green on
|
|
143
|
+
wrong behaviour. It was chosen over a lint/text-grep rule because it checks the **real failure**
|
|
144
|
+
("a call fell through") rather than one way of causing it.
|
|
145
|
+
- **Trade-offs, know them:** (a) a failed `afterEach` **aborts the remaining tests in that spec
|
|
146
|
+
file** (Mocha behaviour), so the first fall-through hides the other results; (b) it is strict —
|
|
147
|
+
when the app starts calling something new the suite fails until someone stubs it. That is the
|
|
148
|
+
intent.
|
|
149
|
+
- **Escape hatch:** `cy.allowUnstubbedApiCalls()` opts **one** test out. Use it only when the
|
|
150
|
+
un-modelled call *is* the point.
|
|
151
|
+
|
|
152
|
+
### The once-per-test check
|
|
153
|
+
|
|
154
|
+
`cy.stubApi()` must be called **exactly once per test** — a second call registers a **second
|
|
155
|
+
catch-all**, and the resulting match order stops the specific intercepts from winning, so a call
|
|
156
|
+
that **is** stubbed falls through anyway. This bit the author's own spec: `stubApi()` ran in both
|
|
157
|
+
`beforeEach` and the test body, `/surfaces/meta?slug=login` fell through, and **the test still
|
|
158
|
+
passed — for the wrong reason**. Fixed by giving that test its own `describe` + `beforeEach`;
|
|
159
|
+
`stubApi()` now **throws** on a second call with the fix in the message. Per-test state is reset
|
|
160
|
+
**only** from the global `beforeEach`, deliberately *not* inside `stubApi()`, so a double call is
|
|
161
|
+
detectable instead of silently self-healing.
|
|
162
|
+
|
|
163
|
+
### The component canary
|
|
164
|
+
|
|
165
|
+
`cypress/component/harness.cy.tsx` mounts a blox component and asserts one React + one
|
|
166
|
+
QueryClient. It fails when the component-test Vite config drifts from `vite.config.ts`.
|
|
167
|
+
|
|
168
|
+
### Not built: CI
|
|
169
|
+
|
|
170
|
+
There is **no** CI for this yet. Worth knowing before you plan it: a workflow must add the four
|
|
171
|
+
`*.togasupply` `/etc/hosts` entries as a step (the tenant is resolved from the hostname) and serve
|
|
172
|
+
a build (`vite preview` / `start-server-and-test`).
|
|
173
|
+
|
|
174
|
+
## How to run it
|
|
175
|
+
|
|
176
|
+
1. `npm run dev` once (any tenant — one server serves all of them).
|
|
177
|
+
2. `npm run cy:e2e:compass` (or `:compasscanada`, `:nychh`, `:quad`, or `cy:e2e:all`).
|
|
178
|
+
3. `npm run cy:ct` for component tests; `cy:open` / `cy:open:<tenant>` for the GUI.
|
|
179
|
+
4. `npm run test:types:cypress` type-checks the specs (`tsc --noEmit -p cypress/tsconfig.json`).
|
|
180
|
+
|
|
181
|
+
Every `cy:*` script goes through **`node scripts/cypress.mjs`** — see the first gotcha below.
|
|
123
182
|
|
|
124
183
|
## Gotchas
|
|
125
184
|
|
|
126
|
-
-
|
|
127
|
-
fails
|
|
128
|
-
|
|
129
|
-
`
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
185
|
+
- **⚠ `bad option: --no-sandbox` is `ELECTRON_RUN_AS_NODE`, NOT a macOS sandbox problem.**
|
|
186
|
+
Cypress fails in *"Verifying Cypress can run"* with
|
|
187
|
+
`.../Cypress.app/Contents/MacOS/Cypress: bad option: --no-sandbox` (also `--smoke-test`,
|
|
188
|
+
`--ping=NNN`). **Cause:** VS Code exports `ELECTRON_RUN_AS_NODE=1` into its integrated terminal
|
|
189
|
+
and into shells its extensions spawn; Cypress's bundled Electron reads it and runs as plain
|
|
190
|
+
Node, which rejects Electron's own flags. **The tell is the wording** — `bad option:` is
|
|
191
|
+
*Node's* format for an unknown CLI flag. Verified 2026-09-03: disabling the tool sandbox does
|
|
192
|
+
**not** help; `env -u ELECTRON_RUN_AS_NODE npx cypress run` works immediately. **Fixed
|
|
193
|
+
permanently** by `scripts/cypress.mjs`, which deletes the variable before spawning (the same
|
|
194
|
+
trap exists on Windows, hence a Node wrapper rather than `env -u`), and all `cy:*` scripts route
|
|
195
|
+
through it. *(This doc previously recorded it as an unfixable environment blocker — it is a
|
|
196
|
+
one-line fix.)*
|
|
197
|
+
- **⚠ NEVER match an intercept with a URL glob — match on `pathname`.** Every request carries a
|
|
198
|
+
query string (blox's axios interceptor appends `transactionId`; `assembleOptions` serializes
|
|
199
|
+
`fields`/`join`/`where` into it) and **some values contain a slash**. Real captured `/domains`
|
|
200
|
+
example: `…AND,SelfDomains.domain:like:http://compass%2Etogasupply)&transactionId=…`.
|
|
201
|
+
minimatch's `*` does not cross `/`, so `**/v2/domains*` **silently fails to match** and the
|
|
202
|
+
catch-all answers instead — which made the login form show *"Error validating domain"* and made
|
|
203
|
+
one test pass for the wrong reason. Use
|
|
204
|
+
`cy.intercept({ method: "GET", pathname: apiPath("domains") }, …)`. Confirmed by mutation:
|
|
205
|
+
reverting one intercept to the glob form made the fall-through guardrail fail and print the URL.
|
|
206
|
+
- **The session key is `user`, not `accessToken`.** blox's `fetchPublicToken` writes an
|
|
207
|
+
`accessToken` (and `refreshToken`) for **logged-out** requests, so a token alone makes an
|
|
208
|
+
anonymous visitor look signed in. `AuthContext` and `useAuthenticationFlow` both key on
|
|
209
|
+
**`user`**. A seeded session must write `accessToken`, `refreshToken`, `user`, `zu-user` (the
|
|
210
|
+
zustand `useUserStore` persist key — the side-nav footer reads name/initials there) and
|
|
211
|
+
`deployment-version` (keeps `useDeploymentGuard` inert).
|
|
212
|
+
- **Seed in `cy.visit`'s `onBeforeLoad`.** Zustand `persist` and the persisted React Query cache
|
|
213
|
+
both rehydrate on **page load only** — an in-app navigation will not pick up storage you wrote
|
|
214
|
+
mid-test.
|
|
215
|
+
- **Clear the persisted query cache between tests.** It lives in localStorage under
|
|
216
|
+
`supply-chain-query-cache`. `src/App.tsx` excludes `surface-meta` / `surface-meta-group` from
|
|
217
|
+
persistence, but **`currentUser` — which carries the ROLE FLAGS — is persisted**, so one test's
|
|
218
|
+
role bleeds into the next unless storage is cleared.
|
|
219
|
+
- **Nav links exist but are not visible.** The side nav is a native `popover="auto"` element (blox
|
|
220
|
+
`SideNav.Content`), so assert `exist`, **not** `be.visible`, unless the test opens the hamburger.
|
|
221
|
+
Items render as `<a href="/path">` because `SideNav.NavigationItem` passes
|
|
222
|
+
`{ as: NavLink, to: path }` through blox's polymorphic `BaseButton`.
|
|
223
|
+
- **Stub `/table-views/meta` with `isSuccess: false`.** blox's `getDataTableMeta` maps that to
|
|
224
|
+
`{ table: null }`, which `useFetchTablePageMeta` documents as the "meta not ready" path. An
|
|
225
|
+
empty **success** envelope instead leaves `table` **undefined**, slips past every downstream
|
|
226
|
+
guard, and crashes in `buildUrl` (`route.includes` on `undefined`).
|
|
227
|
+
- **The envelope's data key is the route name in camelCase:** `purchase-orders` →
|
|
228
|
+
`purchaseOrders`, `core-parameters` → `coreParameters`, `client-authentications` →
|
|
229
|
+
`clientAuthentications`, `table-views` → `tableViews`.
|
|
230
|
+
- **`GET /v2/domains` has two callers with different field sets** — blox `validateEmailDomain`
|
|
231
|
+
(the login form) and `useAuthenticationFlow.checkSsoAuth`, which reads
|
|
232
|
+
`domains[0].ClientAuthentications.type` and **redirects to the IdP** when it is `SSO`. One
|
|
233
|
+
stubbed row typed `PASSWORD` satisfies both and keeps the suite on the local login form. Also:
|
|
234
|
+
`useAuthenticationFlow` returns early when `pathname === "/login"`, so visiting `/login`
|
|
235
|
+
directly never triggers the SSO check.
|
|
236
|
+
- **`surfaces/meta-group`: do not add a sibling key whose value is `null`.** `findSurfaceMap`
|
|
237
|
+
treats a map containing an explicit null as the slug-keyed one, so a `group: null` sibling wins
|
|
238
|
+
over the real map.
|
|
239
|
+
- **Config files type-check green by never being compiled.** `cypress.config.ts` and
|
|
240
|
+
`vitest.config.ts` were checked by **nothing** until both were added to
|
|
241
|
+
`tsconfig.node.json`'s `include` — the same false-green class already documented for this repo
|
|
242
|
+
(the root `tsconfig.json` is references-only, so a bare `npx tsc --noEmit` always exits 0).
|
|
243
|
+
- **`cypress.config.ts` is loaded as NATIVE ESM** by Cypress (`package.json` is `type: module`),
|
|
244
|
+
unlike `vite.config.ts` which Vite transpiles — so `__dirname` does not exist there. Derive it
|
|
245
|
+
with `fileURLToPath(import.meta.url)`.
|
|
246
|
+
- **The component-test Vite config MUST mirror `vite.config.ts`, including the `isBloxLinked`
|
|
247
|
+
branch.** Otherwise mounted components get a second copy of React / react-query — "Invalid hook
|
|
248
|
+
call", or "No QueryClient set" with the provider plainly visible in the stack.
|
|
249
|
+
`component/harness.cy.tsx` is the canary that fails when it drifts.
|
|
250
|
+
- **Scrub captured payloads.** Copying a real body out of Chrome DevTools (Network → filter
|
|
251
|
+
`/v2/` → Response → Copy response) is the fastest way to build a fixture — but those payloads
|
|
252
|
+
carry **real customer data**. Remove emails, names, addresses and tokens before committing.
|
|
253
|
+
|
|
254
|
+
## Rest of the repo's test state (for context)
|
|
255
|
+
|
|
256
|
+
- **Vitest is the other half.** `npm test` (`vitest run`) currently has **one failing test file**:
|
|
257
|
+
`src/hooks/useTalosSurface.test.ts` fails with
|
|
258
|
+
`TypeError: Unknown file extension ".css"` on `@agilant/toga-blox/dist/global.css`. 89 tests pass
|
|
259
|
+
across the other 13 files. **Pre-existing** — the file was last touched in commit `604a779` and
|
|
260
|
+
the Cypress work changed neither it nor `vitest.config.ts`. Needs a separate fix (Vitest CSS
|
|
261
|
+
handling for blox imports).
|
|
262
|
+
- **No other mechanical guardrails exist in this repo:** no `.github/workflows`, no husky, no
|
|
263
|
+
lint-staged. The only `.git/hooks/pre-commit` is a machine-local `code-review-graph` shim that
|
|
264
|
+
always exits 0 and is not checked in.
|
|
265
|
+
- **Green FE tests do not mean a client gets a feature.** Real rollout is a role-scoped
|
|
266
|
+
`SurfaceOverrides IS_VISIBLE` row in `dbchanges2`; the fixture only simulates the grant.
|
|
137
267
|
|
|
138
268
|
## Change history
|
|
139
|
-
- 2026-
|
|
140
|
-
|
|
141
|
-
QUAD
|
|
142
|
-
|
|
143
|
-
(
|
|
269
|
+
- 2026-09-03 — **Rebuilt the harness from scratch on `TRUE-81735`** (the `TRUE-79813` harness this
|
|
270
|
+
doc used to describe was never merged and is stranded there). New shape: all `/v2` calls stubbed,
|
|
271
|
+
four tenants (COMPASS/COMPASSCANADA/NYCHH/QUAD), the three-axes model (client = baseUrl, role =
|
|
272
|
+
`roleFlags` mirroring `resolveRole`, features = surface fixtures), derived `/users` vs fixtured
|
|
273
|
+
surface bundles, and the fall-through + once-per-`stubApi()` guardrails. 26 tests green.
|
|
274
|
+
**Corrected** the old "Cypress's Electron cannot launch in the sandboxed macOS environment"
|
|
275
|
+
gotcha: it is `ELECTRON_RUN_AS_NODE=1` inherited from the VS Code shell, fixed by
|
|
276
|
+
`scripts/cypress.mjs`. Recorded the `pathname`-not-glob intercept rule, the `user`-key session
|
|
277
|
+
facts, and the pre-existing `useTalosSurface.test.ts` Vitest failure. (apeterson)
|
|
278
|
+
- 2026-06-30 — First harness bootstrapped on branch `TRUE-79813` (approval-modal component specs,
|
|
279
|
+
`cy.mountWithProviders`, window-backed hook mocks, fixtures off the real QUAD
|
|
280
|
+
`approvalActionFields.json`). **Never merged**; superseded by the 2026-09-03 rebuild. (apeterson)
|
|
@@ -672,20 +672,96 @@ consuming app, guard against them and move on.
|
|
|
672
672
|
|
|
673
673
|
---
|
|
674
674
|
|
|
675
|
+
## 23. Front-end test layering — **MUST / SHOULD**
|
|
676
|
+
|
|
677
|
+
Pick the layer by **what the test is actually checking**, not by habit. Three layers, three jobs:
|
|
678
|
+
|
|
679
|
+
| Layer | Use it for | Tooling |
|
|
680
|
+
|---|---|---|
|
|
681
|
+
| **Unit / logic** | hooks, view models, resolvers, error classification, URL-state | **Vitest + React Testing Library** (jsdom, co-located `*.test.ts(x)`) |
|
|
682
|
+
| **Component** | the **tenant x role x feature** matrix — the thing that actually breaks | **Cypress component tests** |
|
|
683
|
+
| **e2e** | a handful of real user journeys, end to end | **Cypress e2e** |
|
|
684
|
+
|
|
685
|
+
**MUST — the tenant x role x feature matrix belongs in COMPONENT tests, not e2e.** This is the
|
|
686
|
+
rule most often gotten wrong. Multi-tenant, role-scoped rendering is a *rendering* question: mount
|
|
687
|
+
the component with a tenant + role + surface bundle and assert what appears. Driving that same
|
|
688
|
+
matrix through e2e multiplies runtime and flake for no extra signal, and e2e can only cover one
|
|
689
|
+
tenant per spec anyway (the client *is* the hostname, and Cypress allows one superdomain per spec).
|
|
690
|
+
|
|
691
|
+
**SHOULD — reserve e2e for a few genuine journeys** (log in, land on the default route, navigate).
|
|
692
|
+
Rule of thumb: **no URL change or page transition => it is not an e2e test.** If the assertion is
|
|
693
|
+
"this field/button/column renders for this client and role," that is a component test.
|
|
694
|
+
|
|
695
|
+
**MUST — STUB the surface bundles; never point a front-end test at a backend.** Features and
|
|
696
|
+
permissions are 100% server-driven (the Surface bundles — there is no front-end roster any more),
|
|
697
|
+
so a front-end test must supply them as **fixtures**. A test that reaches a real API is slow,
|
|
698
|
+
non-deterministic, and **can write real data**. Two consequences worth internalizing:
|
|
699
|
+
|
|
700
|
+
- Answer **every** API call, including a **path-scoped catch-all registered first**, and fail the
|
|
701
|
+
test when anything falls through. One check catches four otherwise-silent bugs: a non-matching
|
|
702
|
+
intercept pattern, a typo'd path, a wrong HTTP method, and an endpoint the app newly calls that
|
|
703
|
+
nobody stubbed. All four are silent because the catch-all answers 200 with an empty envelope, so
|
|
704
|
+
the app behaves as if the record did not exist and the spec goes **green on wrong behaviour**.
|
|
705
|
+
- **Derive what you can, fixture only what you must.** Build user/permission records from the
|
|
706
|
+
app's own config so a new field is picked up instead of drifting; fixture only the payloads that
|
|
707
|
+
cannot be derived from anything on the client. Return a field **absent, not null**, when the real
|
|
708
|
+
API would omit it — a stub that always returns everything hides that entire class of bug.
|
|
709
|
+
|
|
710
|
+
**MUST — every new test is SEEN TO FAIL before it is allowed to pass.** A test written straight to
|
|
711
|
+
green has proven nothing; on a stubbed harness a test can easily pass for the wrong reason. Break
|
|
712
|
+
the thing under test on purpose, watch the test go red, then restore it. The two mutations actually
|
|
713
|
+
run against the reference implementation on 2026-09-03, both of which produced the expected red:
|
|
714
|
+
|
|
715
|
+
1. **Break the config the test reads.** The tenant registry was edited to claim `QUAD` is granted
|
|
716
|
+
`transfer-orders` while its surface fixture still withheld it. The nav test failed with
|
|
717
|
+
`Expected to find element: a[href="/transfer-orders"], but never found it.` — proving the test
|
|
718
|
+
reads the real resolved nav, not the registry it was seeded from.
|
|
719
|
+
2. **Break the app code the test covers.** `getUserFieldsForClient` was forced to always return the
|
|
720
|
+
`DEFAULT` field list, dropping every role flag. The user-fields test failed with
|
|
721
|
+
`expected 'firstName,lastName,email,uuid' to include '_isAdmin'` — proving it would catch a real
|
|
722
|
+
regression that silently demotes every admin to `USER`.
|
|
723
|
+
|
|
724
|
+
Mutate **the thing under test**, not the harness. Verifying a *guardrail* (e.g. reverting an
|
|
725
|
+
intercept to a URL glob to confirm the fall-through check fires) is a separate and also worthwhile
|
|
726
|
+
exercise, but it proves the guard works — not that any test does.
|
|
727
|
+
|
|
728
|
+
**MUST — every config file is type-checked.** A config that no `tsconfig` includes is compiled by
|
|
729
|
+
**nothing** and is green by default — the same false-green class as running bare `npx tsc --noEmit`
|
|
730
|
+
against a references-only root `tsconfig.json` (§2). Add `vite.config.ts`, `vitest.config.ts` and
|
|
731
|
+
`cypress.config.ts` to `tsconfig.node.json`'s `include`, and type-check test specs with their own
|
|
732
|
+
project (`tsc --noEmit -p cypress/tsconfig.json`).
|
|
733
|
+
|
|
734
|
+
- ⚠ **`cypress.config.ts` is loaded as NATIVE ESM** by Cypress when `package.json` is
|
|
735
|
+
`type: module` — unlike `vite.config.ts`, which Vite transpiles. So **`__dirname` does not exist
|
|
736
|
+
there**; derive it with `fileURLToPath(import.meta.url)`.
|
|
737
|
+
|
|
738
|
+
> ⚠ **Green front-end tests do NOT mean a client gets a feature.** The fixture only *simulates*
|
|
739
|
+
> the grant. Real rollout is a **role-scoped `SurfaceOverrides IS_VISIBLE` row in `dbchanges2`**.
|
|
740
|
+
> Never close a client-enablement ticket on a passing front-end suite.
|
|
741
|
+
|
|
742
|
+
**Reference implementation:** the worked harness for all of the above is
|
|
743
|
+
[toga25-supply Cypress testing](../apps/toga25-supply/workflows/cypress-testing.md) — three-axis
|
|
744
|
+
tenant model, the stub layer, the fall-through and once-per-`stubApi()` guardrails, and the traps.
|
|
745
|
+
Copy from there rather than reinventing.
|
|
746
|
+
|
|
747
|
+
**Do not** conflate any of this with the team's unresolved **backend** testing question — that is
|
|
748
|
+
**PHP-only** and settled nothing here.
|
|
749
|
+
|
|
750
|
+
---
|
|
751
|
+
|
|
675
752
|
# Part 3 — Open items
|
|
676
753
|
|
|
677
754
|
*(Described and flagged. NOT prescribed as settled.)*
|
|
678
755
|
|
|
679
|
-
- **Front-end testing —
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
team's unresolved *backend* testing question — that is PHP-only.
|
|
756
|
+
- **Front-end testing — SETTLED; moved to §23.** No longer an open item. Test layering (Vitest +
|
|
757
|
+
RTL for logic, Cypress **component** tests for the tenant x role x feature matrix, Cypress e2e
|
|
758
|
+
for a few real journeys), fully-stubbed surface bundles, the see-it-fail rule and config
|
|
759
|
+
type-checking are now prescribed in
|
|
760
|
+
[§23 Front-end test layering](#23-front-end-test-layering--must--should), with the worked harness
|
|
761
|
+
in [toga25-supply Cypress testing](../apps/toga25-supply/workflows/cypress-testing.md). A
|
|
762
|
+
dedicated `frontend-testing.md` sibling remains the likely eventual home if §23 outgrows this
|
|
763
|
+
doc. Still **not** to be conflated with the unresolved *backend* testing question — that is
|
|
764
|
+
PHP-only.
|
|
689
765
|
- **`eslint-plugin-jsx-a11y` not configured** (§10) — a11y is convention-only, not lint-enforced in
|
|
690
766
|
any of the three repos. Recommend adopting it.
|
|
691
767
|
- **Client error-reporting gap.** Verified 2026-08-18: all three repos depend on `@sentry/react`
|
|
@@ -709,6 +785,23 @@ consuming app, guard against them and move on.
|
|
|
709
785
|
|
|
710
786
|
## Change history
|
|
711
787
|
|
|
788
|
+
- 2026-09-03 — §23 correction: replaced the two "seen to fail" worked examples. The first pass
|
|
789
|
+
listed a guardrail check plus an **unverified** component-config mutation; both are now the two
|
|
790
|
+
mutations that were actually run and observed red (a registry/fixture nav mismatch, and forcing
|
|
791
|
+
the `DEFAULT` user field list). Added the distinction that mutating the harness verifies a
|
|
792
|
+
**guard**, not a **test**. (apeterson)
|
|
793
|
+
- 2026-09-03 — **Front-end testing promoted from an open item to §23 (MUST/SHOULD).** Settles the
|
|
794
|
+
layering: Vitest + RTL for logic, Cypress **component** tests for the tenant x role x feature
|
|
795
|
+
matrix (**not** e2e), e2e for a few real journeys only, with the rule of thumb "no URL or page
|
|
796
|
+
transition => not an e2e test". Adds the MUSTs to **stub the surface bundles and never point a
|
|
797
|
+
front-end test at a backend** (path-scoped catch-all registered first + fall-through failure;
|
|
798
|
+
derive what you can, fixture what you must, return absent-not-null), to have **every new test
|
|
799
|
+
seen to fail before it passes** (two worked mutation examples: glob-vs-`pathname` intercept
|
|
800
|
+
matching, and the blox-linked component-config drift canary), and to **type-check every config
|
|
801
|
+
file** — including that `cypress.config.ts` loads as native ESM so `__dirname` is unavailable.
|
|
802
|
+
Warns that green front-end tests do not enable a feature for a client — that is a role-scoped
|
|
803
|
+
`SurfaceOverrides IS_VISIBLE` row in `dbchanges2`. Reference implementation:
|
|
804
|
+
`../apps/toga25-supply/workflows/cypress-testing.md`. (apeterson)
|
|
712
805
|
- 2026-09-03 — §11: replaced supply's stale three-member `dedupe` list with the verified
|
|
713
806
|
seven-member one and added `react-hook-form` (it was in neither `dedupe` nor `alias` while 24
|
|
714
807
|
`src` files import it and blox's `BaseInput` calls `useFormContext()`). Added a SHOULD for
|
package/package.json
CHANGED