From a5df4cad041a7ed9e9c0c09ac4f8146058305c14 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 13:59:59 +0000 Subject: [PATCH] docs(getting-started): write the quick tour's navigation table to the real app (#960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first table a new user reads, whose entire job is "here is what the sidebar holds", had drifted in every one of its eight rows. Measured against `src/apps/crm.app.ts` on `origin/main` (4705aed5, platform 17.0.0-rc.3), the app's `navigation` is one pinned top-level entry plus seven groups: `nav_home` (Home, :34), `group_sales` (:42), `group_work` (My Work, :69), `group_activity` (Activity, :103), `group_marketing` (:120), `group_service` (:131), `group_insights` (Insights, :144) and `group_approvals` (:160). Row by row, the old table said: - **Sales** — the group is real, but it carries nine children, not six. The table dropped Account Workbench (:52), Pipeline (:55) and Sales Performance (:61). - **Service** — real; the entry the table called *Knowledge Base* is labelled **Knowledge** (:138), and **Service Overview** (:139) was missing. Both names were already written to source on `service/index` by #927 / PR #932 and #937 / PR #947, so the two pages contradicted each other. - **Marketing** — real, but its children are Campaigns (:125) and Products (:126). *Campaign Members* is not a navigation entry at all: `grep -rn "crm_campaign_member" src/apps/` returns nothing. - **Products** — no such group. The catalog's only sidebar entry is `nav_product` under `group_marketing`, the same finding #938 / PR #942 and #943 / PR #953 already wrote to two other pages. - **Activities** — no such group; the real one is **Activity**, and `crm_task` is not in it. Its two entries are My Tasks (:84) and All Tasks (:93), both under **My Work** — which the table never mentioned at all, though it is the group a rep uses every day. - **Analytics** — no such group; the real one is **Insights**, and no entry anywhere is labelled *Dashboards* or *Reports*. - **AI** — no such group anywhere in the repo, and no entry labelled *Copilot* or *Knowledge Bases*. `src/apps/` contains one file; neither name appears in it. - **Approvals** — real, with exactly one child, labelled **Inbox** (:165). *Approval Requests* and *Action History* carry no metadata in this repo; #943 / PR #953 recorded the same two names on the revenue page. So the table now lists the pinned Home entry and all seven groups with their real children in source order, and every retired name is re-pointed rather than deleted silently, per the #870 / #877 / #885 / #894 / #913 / #924 / #932 / #942 / #953 convention. Two further facts from source ride along: `group_marketing`, `group_insights` and `group_approvals` declare no `expanded` key while Sales, My Work, Activity and Service set `expanded: true`, and `GroupNavItemSchema.expanded` defaults to `false`, so those three are collapsed on load — the failure mode that makes a reader conclude something is absent. And the zh pages name the labels a simplified-Chinese user actually sees (待我审批, 知识库, 我的工作 — `src/translations/zh-CN.ts:1195-1219`), which is also why the old English *Knowledge Base* read plausibly for so long. Nothing checked any of it: `os validate` and `pnpm lint` walk authored metadata and never open `content/docs`. `test/docs-quick-tour-navigation.test.ts` now compares the table to `CrmApp.navigation` group-for-group and child-for-child in all three locales, pins the bold-is-real / italic-is-phantom typography the sibling pages already use, and pins the source facts the prose rests on. Restoring the old table turns 15 of its 21 assertions red in the predicted direction. Product questions stay open on purpose (#595 / #596): whether Products or AI deserve their own groups is a product decision, not a documentation one. Only the current shape is recorded. All three locales, same section; zh internal links carry no anchor. `src/` untouched. Fixes #960 Claude-Session: https://claude.ai/code/session_01VHrPAGEgFDoHjphqYG4BMa Co-authored-by: Claude --- .../quick-tour-navigation-table-real.md | 28 ++ content/docs/getting-started/quick-tour.mdx | 29 +- .../getting-started/quick-tour.zh-Hans.mdx | 31 +- .../getting-started/quick-tour.zh-Hant.mdx | 31 +- test/docs-quick-tour-navigation.test.ts | 282 ++++++++++++++++++ 5 files changed, 374 insertions(+), 27 deletions(-) create mode 100644 .changeset/quick-tour-navigation-table-real.md create mode 100644 test/docs-quick-tour-navigation.test.ts diff --git a/.changeset/quick-tour-navigation-table-real.md b/.changeset/quick-tour-navigation-table-real.md new file mode 100644 index 00000000..7a59e92a --- /dev/null +++ b/.changeset/quick-tour-navigation-table-real.md @@ -0,0 +1,28 @@ +--- +'hotcrm': patch +--- + +Rewrite the quick tour's left-navigation table against the app's real navigation. +The table in `content/docs/getting-started/quick-tour.mdx` — the first thing a new +user reads, and a table whose entire job is "here is what the sidebar holds" — had +drifted in every one of its eight rows. Four of the groups it named do not exist in +`src/apps/crm.app.ts` (*Products*, *Activities*, *Analytics*, *AI*), three groups +that do exist were absent altogether (**My Work**, **Activity**, **Insights**), and +the four rows whose group was real each dropped items or used a label the app never +shows: **Sales** was missing **Account Workbench**, **Pipeline** and **Sales +Performance**; **Service** spelled *Knowledge Base* for the entry actually labelled +**Knowledge** and omitted **Service Overview**; **Marketing** listed *Campaign +Members*, which is not a sidebar item at all; and **Approvals** listed two items +where the group has exactly one, **Inbox**. + +The table now carries the pinned **Home** entry and all seven groups with their real +children, in source order, and says which groups are collapsed when the app loads +(**Marketing**, **Insights**, **Approvals**) — the failure mode that makes a reader +conclude something is missing. Every retired name is re-pointed rather than deleted: +the catalog is the **Products** item under **Marketing**, tasks are **My Tasks** and +**All Tasks** under **My Work**, the Copilot is the right-side chat panel, campaign +membership is reached from the campaign detail page, and the approval audit trail is +real data (`sys_approval_action`) that no navigation entry opens. All three locales +updated; `src/` untouched. A new guard, `test/docs-quick-tour-navigation.test.ts`, +compares the table against `CrmApp.navigation` group-for-group and child-for-child in +all three locales, so the next navigation change cannot leave the tour behind. diff --git a/content/docs/getting-started/quick-tour.mdx b/content/docs/getting-started/quick-tour.mdx index 25d0e22a..a9ae8384 100644 --- a/content/docs/getting-started/quick-tour.mdx +++ b/content/docs/getting-started/quick-tour.mdx @@ -30,18 +30,29 @@ Click the **app launcher** (top-left grid icon) to switch between apps: - **Enterprise CRM** — the main app with all CRM functionality. - **Admin** — for system administrators. -Inside Enterprise CRM, the left nav groups things by: +Inside Enterprise CRM, the left nav opens with one pinned item — **Home**, which is the **Executive Overview** dashboard — and everything else sits in seven groups: | Group | Contents | | --- | --- | -| **Sales** | Leads, Accounts, Contacts, Opportunities, Quotes, Contracts | -| **Service** | Cases, Knowledge Base | -| **Marketing** | Campaigns, Campaign Members | -| **Products** | Products | -| **Activities** | Tasks, Events | -| **Analytics** | Dashboards, Reports | -| **AI** | Copilot, Knowledge Bases | -| **Approvals** | Approval Requests, Action History | +| **Sales** | Leads, Accounts, Account Workbench, Contacts, Opportunities, Pipeline, Quotes, Contracts, Sales Performance | +| **My Work** | My Tasks, My Deals, My Leads, My Cases, My Calendar, All Tasks | +| **Activity** | Events, Calendar, Interaction History, Sales Activity | +| **Marketing** | Campaigns, Products | +| **Service** | Cases, Knowledge, Service Overview | +| **Insights** | CRM Overview, Forecasts, Pipeline Coverage, Lead Inflow, SLA Performance | +| **Approvals** | Inbox | + +**Marketing**, **Insights** and **Approvals** are collapsed when the app loads — only **Sales**, **My Work**, **Activity** and **Service** open themselves. Click a group open before deciding something isn't there. + +Four group names this page used to list are not in the app at all, and several item names sit somewhere other than where it put them: + +- *Products* is not a group. The catalog is the **Products** item under **Marketing** — see [Marketing](/docs/marketing/index). +- *Activities* is not a group; the real one is **Activity**, and tasks are not in it. `crm_task` reaches the sidebar as **My Tasks** and **All Tasks**, both under **My Work**. +- *Analytics* is not a group; the real one is **Insights**. Nothing in it is called *Dashboards* or *Reports* — the dashboard and the three reports it pins are listed above under their own names. +- *AI* is not a group, and no sidebar item is called *Copilot* or *Knowledge Bases*. The Copilot is the right-side chat panel on every page (section 6 below), and the knowledge articles are **Knowledge** under **Service**. +- *Knowledge Base* is not a label this app carries. The **Service** entry is **Knowledge**, and the service dashboard beside it is **Service Overview** — see [Service](/docs/service/index). +- *Campaign Members* is not a sidebar item. Campaign membership is reached from the campaign detail page or from global search — see [Campaign Members](/docs/marketing/campaign-members). +- *Approval Requests* and *Action History* are not items either: **Inbox** is the only entry under **Approvals**. The approval audit trail is real data — the approvals plugin stores it as `sys_approval_action` — but no navigation entry in this app opens it. ## 3. Your first lead diff --git a/content/docs/getting-started/quick-tour.zh-Hans.mdx b/content/docs/getting-started/quick-tour.zh-Hans.mdx index d1d94fa4..bdba8cbb 100644 --- a/content/docs/getting-started/quick-tour.zh-Hans.mdx +++ b/content/docs/getting-started/quick-tour.zh-Hans.mdx @@ -30,18 +30,31 @@ description: HotCRM 的 5 分钟演练 —— 主页、你的第一条线索、 - **Enterprise CRM** —— 包含所有 CRM 功能的主应用。 - **Admin** —— 面向系统管理员。 -在 Enterprise CRM 内,左侧导航按以下方式分组: +在 Enterprise CRM 内,左侧导航最上面是一个固定条目——**Home**,它打开的是 **Executive Overview** 仪表盘;其余内容分在 7 个分组里: | 分组 | 内容 | | --- | --- | -| **Sales** | Leads、Accounts、Contacts、Opportunities、Quotes、Contracts | -| **Service** | Cases、Knowledge Base | -| **Marketing** | Campaigns、Campaign Members | -| **Products** | Products | -| **Activities** | Tasks、Events | -| **Analytics** | Dashboards、Reports | -| **AI** | Copilot、Knowledge Bases | -| **Approvals** | Approval Requests、Action History | +| **Sales** | Leads、Accounts、Account Workbench、Contacts、Opportunities、Pipeline、Quotes、Contracts、Sales Performance | +| **My Work** | My Tasks、My Deals、My Leads、My Cases、My Calendar、All Tasks | +| **Activity** | Events、Calendar、Interaction History、Sales Activity | +| **Marketing** | Campaigns、Products | +| **Service** | Cases、Knowledge、Service Overview | +| **Insights** | CRM Overview、Forecasts、Pipeline Coverage、Lead Inflow、SLA Performance | +| **Approvals** | Inbox | + +上表用的是源码里的英文 label,也就是英文界面上看到的名字。简体中文界面下,它们显示为语言包里的译名——例如 **Inbox** 显示为「待我审批」、**Knowledge** 显示为「知识库」、**My Work** 显示为「我的工作」。 + +**Marketing**、**Insights**、**Approvals** 这三个分组在应用加载时是折叠的——只有 **Sales**、**My Work**、**Activity**、**Service** 会自动展开。先把分组点开,再判断某样东西在不在这里。 + +本页过去列出的 4 个分组名,应用里根本不存在;另有几个条目名,真身并不在它说的位置: + +- *Products*(产品)不是分组。产品目录是 **Marketing** 分组下的 **Products** 条目——参见 [Marketing](/zh-Hans/docs/marketing/index)。 +- *Activities* 不是分组,真实的分组叫 **Activity**,而且任务不在里面。`crm_task` 在侧边栏上是 **My Tasks** 与 **All Tasks** 两项,都挂在 **My Work** 分组下。 +- *Analytics* 不是分组,真实的分组叫 **Insights**。它里面没有任何条目叫 *Dashboards* 或 *Reports*——它固定的那个仪表盘和三张报表,上表已按各自的名字列出。 +- *AI* 不是分组,也没有叫 *Copilot* 或 *Knowledge Bases* 的侧边栏条目。Copilot 是每个页面右侧的聊天面板(见下文第 6 节),知识文章则是 **Service** 分组下的 **Knowledge**。 +- *Knowledge Base* 不是本应用使用的 label。**Service** 分组下的条目叫 **Knowledge**,与它并列的服务仪表盘叫 **Service Overview**——参见 [Service](/zh-Hans/docs/service/index)。 +- *Campaign Members* 不是侧边栏条目。营销活动成员要从营销活动详情页或全局搜索进入——参见 [Campaign Members](/zh-Hans/docs/marketing/campaign-members)。 +- *Approval Requests* 与 *Action History* 也都不是条目:**Approvals** 分组下只有 **Inbox** 一项。审批的操作留痕本身是真实数据,由审批插件以 `sys_approval_action` 存储,但本应用里没有任何导航条目能打开它。 ## 3. 你的第一条线索 diff --git a/content/docs/getting-started/quick-tour.zh-Hant.mdx b/content/docs/getting-started/quick-tour.zh-Hant.mdx index bb41cddd..a25defb8 100644 --- a/content/docs/getting-started/quick-tour.zh-Hant.mdx +++ b/content/docs/getting-started/quick-tour.zh-Hant.mdx @@ -28,18 +28,31 @@ description: HotCRM 的 5 分鐘演練 —— 主頁、你的第一條線索、 - **Enterprise CRM** —— 包含所有 CRM 功能的主應用。 - **Admin** —— 面向系統管理員。 -在 Enterprise CRM 內,左側導覽按以下方式分組: +在 Enterprise CRM 內,左側導覽最上面是一個固定條目——**Home**,它打開的是 **Executive Overview** 儀表板;其餘內容分在 7 個群組裡: | 分組 | 內容 | | --- | --- | -| **Sales** | Leads、Accounts、Contacts、Opportunities、Quotes、Contracts | -| **Service** | Cases、Knowledge Base | -| **Marketing** | Campaigns、Campaign Members | -| **Products** | Products | -| **Activities** | Tasks、Events | -| **Analytics** | Dashboards、Reports | -| **AI** | Copilot、Knowledge Bases | -| **Approvals** | Approval Requests、Action History | +| **Sales** | Leads、Accounts、Account Workbench、Contacts、Opportunities、Pipeline、Quotes、Contracts、Sales Performance | +| **My Work** | My Tasks、My Deals、My Leads、My Cases、My Calendar、All Tasks | +| **Activity** | Events、Calendar、Interaction History、Sales Activity | +| **Marketing** | Campaigns、Products | +| **Service** | Cases、Knowledge、Service Overview | +| **Insights** | CRM Overview、Forecasts、Pipeline Coverage、Lead Inflow、SLA Performance | +| **Approvals** | Inbox | + +上表用的是原始碼裡的英文 label,也就是英文介面上看到的名字。本應用提供 en / zh-CN / ja-JP / es-ES 四種介面語言,沒有繁體語言包——簡體中文介面下這些名字會顯示為語言包裡的譯名,例如 **Inbox** 顯示為「待我审批」、**Knowledge** 顯示為「知识库」、**My Work** 顯示為「我的工作」。 + +**Marketing**、**Insights**、**Approvals** 這三個群組在應用載入時是摺疊的——只有 **Sales**、**My Work**、**Activity**、**Service** 會自動展開。先把群組點開,再判斷某樣東西在不在這裡。 + +本頁過去列出的 4 個群組名,應用裡根本不存在;另有幾個條目名,真身並不在它說的位置: + +- *Products*(產品)不是群組。產品目錄是 **Marketing** 群組下的 **Products** 條目——參見 [Marketing](/zh-Hant/docs/marketing/index)。 +- *Activities* 不是群組,真實的群組叫 **Activity**,而且任務不在裡面。`crm_task` 在側邊欄上是 **My Tasks** 與 **All Tasks** 兩項,都掛在 **My Work** 群組下。 +- *Analytics* 不是群組,真實的群組叫 **Insights**。它裡面沒有任何條目叫 *Dashboards* 或 *Reports*——它釘住的那個儀表板和三張報表,上表已按各自的名字列出。 +- *AI* 不是群組,也沒有叫 *Copilot* 或 *Knowledge Bases* 的側邊欄條目。Copilot 是每個頁面右側的聊天面板(見下文第 6 節),知識文章則是 **Service** 群組下的 **Knowledge**。 +- *Knowledge Base* 不是本應用使用的 label。**Service** 群組下的條目叫 **Knowledge**,與它並列的服務儀表板叫 **Service Overview**——參見 [Service](/zh-Hant/docs/service/index)。 +- *Campaign Members* 不是側邊欄條目。行銷活動成員要從行銷活動詳情頁或全域搜尋進入——參見 [Campaign Members](/zh-Hant/docs/marketing/campaign-members)。 +- *Approval Requests* 與 *Action History* 也都不是條目:**Approvals** 群組下只有 **Inbox** 一項。審批的操作留痕本身是真實資料,由審批外掛以 `sys_approval_action` 儲存,但本應用裡沒有任何導覽條目能打開它。 ## 3. 你的第一條線索 diff --git a/test/docs-quick-tour-navigation.test.ts b/test/docs-quick-tour-navigation.test.ts new file mode 100644 index 00000000..5f77a53b --- /dev/null +++ b/test/docs-quick-tour-navigation.test.ts @@ -0,0 +1,282 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { REPO_ROOT } from './helpers/repo-root'; +import { CrmApp } from '../src/apps/crm.app'; +import { ExecutiveDashboard } from '../src/dashboards/executive.dashboard'; + +/** + * The quick-tour page's left-nav table, pinned to `src/apps/crm.app.ts` (#960). + * + * This is the first table a new user reads, and its entire job is "here is what + * the sidebar holds". Every one of its eight rows had drifted: four named + * groups the app does not have (*Products*, *Activities*, *Analytics*, *AI*), + * three real groups were absent entirely (**My Work**, **Activity**, + * **Insights**), and the four rows whose group did exist each dropped items or + * spelled a label the app never shows (*Knowledge Base* for **Knowledge**, + * *Campaign Members* / *Approval Requests* / *Action History* for items that + * exist nowhere). + * + * Nothing caught it: `os validate` and `pnpm lint` walk authored metadata and + * never open `content/docs`, so — as with the service-index guard in + * `docs-service-index-analytics.test.ts` (#948) — the check has to live where + * the claim lives. Prose that enumerates an IA goes stale the first time the IA + * moves, which is exactly what happened here. + * + * The rules below are two-directional, and the page's typography carries the + * distinction: + * + * - **bold** is reserved for names the app really carries. Every bold Latin run + * in the section must resolve to a navigation label or to the dashboard the + * pinned Home entry opens. (Bold runs containing CJK are the locale prose's + * own emphasis and are skipped — product names stay English in every locale, + * the same convention `docs-drift.test.ts` leans on.) + * - *italic* is reserved for a name the product does NOT carry, the spelling + * #927 / PR #932 established. Every retired name must still be named in + * italics — the convention is to say where a reader's name really lives, not + * to delete it silently — and no phantom may ever appear in bold. + * - exists ⇒ listed: the table's rows are compared against `CrmApp.navigation` + * group-for-group, child-for-child, in source order. Add a nav item without + * touching the tour and this file goes red at PR time, in all three locales. + */ + +type AnyRec = Record; + +const NAV = ((CrmApp as AnyRec).navigation ?? []) as AnyRec[]; +const GROUPS = NAV.filter((n) => n.type === 'group'); +const PINNED = NAV.filter((n) => n.type !== 'group'); + +const childLabels = (g: AnyRec): string[] => + ((g.children ?? []) as AnyRec[]).map((c) => c.label as string); + +const GROUP_LABELS: string[] = GROUPS.map((g) => g.label as string); +const ALL_NAV_LABELS: string[] = [ + ...PINNED.map((p) => p.label as string), + ...GROUP_LABELS, + ...GROUPS.flatMap(childLabels), +]; + +/** Groups that do NOT set `expanded: true` — `GroupNavItemSchema.expanded` defaults to false. */ +const COLLAPSED: string[] = GROUPS.filter((g) => g.expanded !== true).map((g) => g.label as string); +const EXPANDED: string[] = GROUPS.filter((g) => g.expanded === true).map((g) => g.label as string); + +const ALLOWED_BOLD = new Set([...ALL_NAV_LABELS, ExecutiveDashboard.label as string]); + +/** + * Names the old table carried that resolve to no navigation label anywhere. + * Each must be named in italics and must never be bolded. + */ +const PHANTOMS = [ + 'Activities', + 'Analytics', + 'AI', + 'Copilot', + 'Knowledge Bases', + 'Knowledge Base', + 'Campaign Members', + 'Approval Requests', + 'Action History', + 'Dashboards', + 'Reports', +] as const; + +/** + * A name the old table used as a GROUP that is real only as an ITEM. It must be + * named in italics (as the non-existent group) and may also appear in bold (as + * the real item under Marketing), so it is exempt from the never-bold rule. + */ +const RETIRED_GROUP_NAMES = ['Products'] as const; + +const PAGES = [ + { + file: 'content/docs/getting-started/quick-tour.mdx', + lang: 'en', + /** First line of the guarded block; the block runs to the next `## `. */ + start: 'Inside Enterprise CRM,', + sep: ', ', + collapse: /collapsed when the app loads/, + }, + { + file: 'content/docs/getting-started/quick-tour.zh-Hans.mdx', + lang: 'zh-Hans', + start: '在 Enterprise CRM 内,', + sep: '、', + collapse: /在应用加载时是折叠的/, + }, + { + file: 'content/docs/getting-started/quick-tour.zh-Hant.mdx', + lang: 'zh-Hant', + start: '在 Enterprise CRM 內,', + sep: '、', + collapse: /在應用載入時是摺疊的/, + }, +] as const; + +const blockOf = (file: string, start: string): string => { + const lines = readFileSync(join(REPO_ROOT, file), 'utf8').split('\n'); + const from = lines.findIndex((l) => l.startsWith(start)); + expect(from, `${file}: block start '${start}' not found`).toBeGreaterThanOrEqual(0); + const rest = lines.slice(from); + const end = rest.findIndex((l, i) => i > 0 && l.startsWith('## ')); + return (end === -1 ? rest : rest.slice(0, end)).join('\n'); +}; + +/** Body rows of the block's one markdown table, as trimmed cell arrays. */ +const tableRows = (block: string): string[][] => + block + .split('\n') + .filter((l) => l.trim().startsWith('|')) + .map((l) => + l + .trim() + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map((c) => c.trim()), + ) + .filter((cells) => !cells.every((c) => /^:?-{2,}:?$/.test(c))) + .slice(1); // drop the header row + +/** + * The CJK range is written as escapes rather than literal characters so this + * file stays greppable in a repo whose tooling scans it as text. + */ +const CJK = /[\u3400-\u9fff]/; + +const boldNames = (block: string): string[] => + [...block.matchAll(/\*\*([^*\n]+)\*\*/g)].map((m) => m[1].trim()).filter((s) => !CJK.test(s)); + +const italicNames = (block: string): string[] => + [...block.replace(/\*\*[^*\n]+\*\*/g, '').matchAll(/\*([^*\n]+)\*/g)].map((m) => m[1].trim()); + +describe('getting-started/quick-tour names the navigation the app really ships (#960)', () => { + describe.each(PAGES)('$file', ({ file, start, sep, collapse }) => { + const block = () => blockOf(file, start); + + it('lists every group, in source order, with exactly its children', () => { + const rows = tableRows(block()); + expect( + rows.map((r) => r[0]), + `${file}: the table's group column must be every CrmApp group, in source order`, + ).toEqual(GROUP_LABELS.map((l) => `**${l}**`)); + + GROUPS.forEach((g, i) => { + expect( + rows[i][1].split(sep).map((s) => s.trim()), + `${file}: contents of the ${g.label} row`, + ).toEqual(childLabels(g)); + }); + }); + + it('names the pinned top-level entry and the dashboard it opens', () => { + const text = block(); + PINNED.forEach((p) => { + expect(text, `${file}: pinned nav item '${p.label}' is not named`).toContain( + `**${p.label}**`, + ); + }); + expect(text).toContain(`**${ExecutiveDashboard.label}**`); + }); + + it('bolds only names the app actually carries', () => { + const unknown = boldNames(block()).filter((n) => !ALLOWED_BOLD.has(n)); + expect( + unknown, + `${file}: bolded name(s) that are not a navigation label or the pinned dashboard's ` + + 'label. Bold is reserved for real names here — a name the app does not carry goes ' + + 'in *italics*.', + ).toEqual([]); + }); + + it('still names every retired name, in italics, and never in bold', () => { + const text = block(); + const italics = italicNames(text); + const bold = boldNames(text); + + const unnamed = [...PHANTOMS, ...RETIRED_GROUP_NAMES].filter((n) => !italics.includes(n)); + expect( + unnamed, + `${file}: a name the old table carried was dropped instead of re-pointed. Readers ` + + 'arrive with these names — say where the thing really is, do not delete it silently.', + ).toEqual([]); + + const promoted = PHANTOMS.filter((n) => bold.includes(n)); + expect( + promoted, + `${file}: name(s) the app does not carry, written in bold as if they were real`, + ).toEqual([]); + }); + + it('says which groups are collapsed when the app loads, and which open themselves', () => { + const text = block(); + expect(text, `${file}: the default-collapse note is missing`).toMatch(collapse); + const line = text.split('\n').find((l) => collapse.test(l)) ?? ''; + COLLAPSED.forEach((l) => + expect(line, `${file}: '${l}' is collapsed on load but the note omits it`).toContain( + `**${l}**`, + ), + ); + EXPANDED.forEach((l) => + expect(line, `${file}: '${l}' opens itself but the note omits it`).toContain(`**${l}**`), + ); + }); + }); +}); + +describe('the source facts the quick-tour table now rests on (#960)', () => { + it('the app has one pinned top-level entry and seven groups', () => { + expect(PINNED.map((p) => p.label)).toEqual(['Home']); + expect(GROUP_LABELS).toEqual([ + 'Sales', + 'My Work', + 'Activity', + 'Marketing', + 'Service', + 'Insights', + 'Approvals', + ]); + }); + + it('Home opens the executive dashboard, not the CRM overview one', () => { + expect(PINNED[0].type).toBe('dashboard'); + expect(PINNED[0].dashboardName).toBe(ExecutiveDashboard.name); + }); + + it('Marketing, Insights and Approvals are the groups that stay collapsed', () => { + expect(COLLAPSED).toEqual(['Marketing', 'Insights', 'Approvals']); + expect(EXPANDED).toEqual(['Sales', 'My Work', 'Activity', 'Service']); + }); + + it('carries no navigation label matching any name the page calls a phantom', () => { + const labels = new Set(ALL_NAV_LABELS); + expect(PHANTOMS.filter((n) => labels.has(n))).toEqual([]); + }); + + it('routes crm_task through My Work only, and reaches campaign members from no nav item', () => { + const walk = (nodes: AnyRec[]): AnyRec[] => + nodes.flatMap((n) => [n, ...walk((n.children ?? []) as AnyRec[])]); + const all = walk(NAV); + expect(all.filter((n) => n.objectName === 'crm_campaign_member')).toEqual([]); + + const taskOwners = GROUPS.filter((g) => + ((g.children ?? []) as AnyRec[]).some((c) => c.objectName === 'crm_task'), + ).map((g) => g.label); + expect(taskOwners).toEqual(['My Work']); + }); + + it('keeps the labels the page spells verbatim', () => { + const byId = new Map( + ((): AnyRec[] => { + const walk = (nodes: AnyRec[]): AnyRec[] => + nodes.flatMap((n) => [n, ...walk((n.children ?? []) as AnyRec[])]); + return walk(NAV); + })().map((n) => [n.id as string, n]), + ); + expect(byId.get('nav_knowledge')?.label).toBe('Knowledge'); + expect(byId.get('nav_service_dashboard')?.label).toBe('Service Overview'); + expect(byId.get('nav_approval_requests')?.label).toBe('Inbox'); + expect(byId.get('nav_product')?.label).toBe('Products'); + }); +});