@salesforcedevs/sfdocs-e2e 1.0.11 → 1.0.12-e2e-topic-version-2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/sfdocs-e2e",
3
- "version": "1.0.11",
3
+ "version": "1.0.12-e2e-topic-version-2",
4
4
  "description": "E2E test suite for SFDocs static portal pages (SSG)",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -40,6 +40,92 @@ describe('Test Sitemap Routes', () => {
40
40
  );
41
41
  });
42
42
 
43
+ it('lists each docs markdown page once and each spec page without a .html extension', async () => {
44
+ await HomePage.open('sitemap.xml');
45
+ const data = await browser.getSitemap(
46
+ 'http://localhost:3000/internal-sitemap-contents-1.xml',
47
+ );
48
+
49
+ // Markdown topics are published with a .html extension.
50
+ expect(data.body).toContain('<loc>/docs/docs-content-type-example/get-started.html</loc>');
51
+ expect(data.body).toContain(
52
+ '<loc>/docs/docs-content-type-example/widget-concepts.html</loc>',
53
+ );
54
+ expect(data.body).toContain(
55
+ '<loc>/docs/docs-content-type-example/beta-bulk-import.html</loc>',
56
+ );
57
+
58
+ // Spec (Redoc) topics are published without an extension.
59
+ expect(data.body).toContain(
60
+ '<loc>/docs/docs-content-type-example/widgets-api-reference</loc>',
61
+ );
62
+ expect(data.body).toContain('<loc>/docs/docs-content-type-example/new-sample-api</loc>');
63
+
64
+ // Each markdown URL appears exactly once.
65
+ const occurrences =
66
+ data.body.split('<loc>/docs/docs-content-type-example/get-started.html</loc>').length -
67
+ 1;
68
+ expect(occurrences).toBe(1);
69
+ });
70
+
71
+ it('excludes the external-link topic from the docs sitemap', async () => {
72
+ await HomePage.open('sitemap.xml');
73
+ const data = await browser.getSitemap(
74
+ 'http://localhost:3000/internal-sitemap-contents-1.xml',
75
+ );
76
+
77
+ // externalLink entries never become routes, so they can't reach the sitemap.
78
+ expect(data.body).not.toContain('developer.salesforce.com');
79
+ });
80
+
81
+ it('lists the deep-nesting docs project pages and spec in the sitemap', async () => {
82
+ await HomePage.open('sitemap.xml');
83
+ const data = await browser.getSitemap(
84
+ 'http://localhost:3000/internal-sitemap-contents-1.xml',
85
+ );
86
+
87
+ expect(data.body).toContain('<loc>/docs/docs-deep-nesting-example/introduction.html</loc>');
88
+ expect(data.body).toContain('<loc>/docs/docs-deep-nesting-example/install.html</loc>');
89
+ expect(data.body).toContain('<loc>/docs/docs-deep-nesting-example/workspace.html</loc>');
90
+ expect(data.body).toContain('<loc>/docs/docs-deep-nesting-example/example-api</loc>');
91
+ });
92
+
93
+ it('lists a localized docs page but omits pages excluded from the ja-jp menu', async () => {
94
+ await HomePage.open('sitemap.xml');
95
+ const data = await browser.getSitemap(
96
+ 'http://localhost:3000/internal-sitemap-contents-1.xml',
97
+ );
98
+
99
+ // ja-jp topics that are configured in the ja-jp TOC are legitimately in the sitemap.
100
+ expect(data.body).toContain(
101
+ '<loc>/docs/ja-jp/docs-content-type-example/get-started.html</loc>',
102
+ );
103
+
104
+ // englishonly pages are never in the ja-jp menu, so they must not appear under /ja-jp/.
105
+ expect(data.body).not.toContain(
106
+ '<loc>/docs/ja-jp/docs-content-type-example/us-compliance-notes.html</loc>',
107
+ );
108
+ });
109
+
110
+ it('serves the noindex robots metatag on the docs home URL but not on a real topic', async () => {
111
+ await browser.addCommand('getHtml', async (url: any) => {
112
+ const data = await got(url);
113
+ return data;
114
+ });
115
+
116
+ // The project home URL renders the first topic as a landing page with noindex.
117
+ const home = await browser.getHtml('http://localhost:3000/docs/docs-content-type-example');
118
+ expect(home.body).toContain('<meta name="robots" content="noindex,nofollow,nosnippet"/>');
119
+
120
+ // The same topic requested at its real URL must be indexable.
121
+ const topic = await browser.getHtml(
122
+ 'http://localhost:3000/docs/docs-content-type-example/get-started.html',
123
+ );
124
+ expect(topic.body).not.toContain(
125
+ '<meta name="robots" content="noindex,nofollow,nosnippet"/>',
126
+ );
127
+ });
128
+
43
129
  it('Read Coveo sitemap.xml', async () => {
44
130
  await browser.addCommand('getCoveoSitemap', async (url: any) => {
45
131
  const data = await got(url, {
@@ -33,7 +33,7 @@ class DocsPage extends Page {
33
33
  }
34
34
 
35
35
  private async getTreeTile(treeItem: WebdriverIO.Element): Promise<WebdriverIO.Element> {
36
- return treeItem.shadow$('a > dx-tree-tile');
36
+ return this.getTreeItemTile(treeItem);
37
37
  }
38
38
 
39
39
  private async getTreeItemLabel(treeItem: WebdriverIO.Element): Promise<string> {
@@ -82,6 +82,11 @@ class DocsPage extends Page {
82
82
  return (await this.getTreeTile(treeItem)).shadow$('.tile-external-link-icon');
83
83
  }
84
84
 
85
+ async getTileAnchor(label: string): Promise<WebdriverIO.Element> {
86
+ const treeItem = await this.getSidebarTile(label);
87
+ return (await this.getTreeTile(treeItem)).shadow$('a');
88
+ }
89
+
85
90
  async getRedocBackLink(): Promise<WebdriverIO.Element> {
86
91
  return $('>>>a.redoc-project-back');
87
92
  }
@@ -84,6 +84,25 @@ export default class Page {
84
84
  return await $('>>>doc-specification-content');
85
85
  }
86
86
 
87
+ async getTreeItemTile(treeItem: WebdriverIO.Element): Promise<WebdriverIO.Element> {
88
+ return treeItem.shadow$('dx-tree-tile');
89
+ }
90
+
91
+ /**
92
+ * Options render in a single shared overlay, one per picker on the page, hidden until their
93
+ * own dropdown opens — filtering to visible ones scopes this to whichever just opened.
94
+ */
95
+ async getVersionOptions(): Promise<WebdriverIO.Element[]> {
96
+ const allOptions = await $$('>>>doc-version-picker dx-dropdown-option');
97
+ const visible: WebdriverIO.Element[] = [];
98
+ for (const option of allOptions) {
99
+ if (await option.isDisplayed()) {
100
+ visible.push(option);
101
+ }
102
+ }
103
+ return visible;
104
+ }
105
+
87
106
  async waitForSpinnerToDisappear(element: WebdriverIO.Element): Promise<void> {
88
107
  await browser.waitUntil(
89
108
  async function () {
@@ -0,0 +1,196 @@
1
+ import Page from './page';
2
+
3
+ /**
4
+ * Page object for topic-level (per-topic) version picker interactions.
5
+ *
6
+ * Two `doc-version-picker` instances exist for a versioned topic:
7
+ * - the SIDEBAR picker (small variant), rendered as a sibling of `dx-tree-tile` inside the
8
+ * versioned tree row's own shadow root (`class="tree-item-version-picker"`).
9
+ * - the REDOC SPEC-PAGE picker (default variant), rendered inside `div.redoc-project-header`
10
+ * alongside the back-link/spec title. Redoc mounts this header into BOTH `.menu-content` and
11
+ * `.api-content`, so we always scope through the sidebar's copy (`getRedocProjectHeader`) to
12
+ * avoid matching the duplicate.
13
+ */
14
+ export default class TopicVersionPage extends Page {
15
+ async waitForMarkdownTopicLoad(): Promise<void> {
16
+ await (
17
+ await $('>>>doc-content-layout')
18
+ ).waitForDisplayed({ timeout: this.REFERENCE_LOAD_WAIT_TIME });
19
+ }
20
+
21
+ async waitForRedocTopicLoad(): Promise<void> {
22
+ await $('doc-redoc-reference > .redoc-container').waitForDisplayed({
23
+ timeout: this.REFERENCE_LOAD_WAIT_TIME,
24
+ });
25
+ }
26
+
27
+ // --- Sidebar tree traversal (mirrors docs.page.ts's private helpers, exposed here so
28
+ // individual rows and their pickers can be located directly) ---
29
+
30
+ async getSidebar(): Promise<WebdriverIO.Element> {
31
+ return (await $('>>>doc-content-layout')).shadow$('div > dx-sidebar-old');
32
+ }
33
+
34
+ async waitForSidebar(): Promise<void> {
35
+ await (await this.getSidebar()).waitForDisplayed();
36
+ }
37
+
38
+ private async getTreeItemLabel(treeItem: WebdriverIO.Element): Promise<string> {
39
+ const tile = await this.getTreeItemTile(treeItem);
40
+ const label = await tile.shadow$('span.tile-label');
41
+ return (await label.getText()).trim();
42
+ }
43
+
44
+ private async findTreeItemByLabel(
45
+ treeItem: WebdriverIO.Element,
46
+ label: string,
47
+ ): Promise<WebdriverIO.Element | null> {
48
+ if ((await this.getTreeItemLabel(treeItem)) === label) {
49
+ return treeItem;
50
+ }
51
+
52
+ const children = await treeItem.shadow$$('dx-tree-item');
53
+ for (const child of children) {
54
+ const match = await this.findTreeItemByLabel(child, label);
55
+ if (match) {
56
+ return match;
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /** Locates a sidebar tree row (dx-tree-item) by its visible label. */
63
+ async getSidebarTreeItem(label: string): Promise<WebdriverIO.Element> {
64
+ const sidebar = await this.getSidebar();
65
+ const trees = await sidebar.shadow$$('div.container div.sidebar-content dx-tree');
66
+ for (const tree of trees) {
67
+ const rootItem = await tree.shadow$('dx-tree-item');
68
+ const match = await this.findTreeItemByLabel(rootItem, label);
69
+ if (match) {
70
+ return match;
71
+ }
72
+ }
73
+ throw new Error(`No sidebar tree item found with label "${label}"`);
74
+ }
75
+
76
+ /** The sidebar row's own doc-version-picker (small variant), or null if it has none. */
77
+ async getSidebarPicker(rowLabel: string): Promise<WebdriverIO.Element | null> {
78
+ const treeItem = await this.getSidebarTreeItem(rowLabel);
79
+ const picker = await treeItem.shadow$('.tree-item-version-picker');
80
+ return (await picker.isExisting()) ? picker : null;
81
+ }
82
+
83
+ // --- Redoc spec-page project header (default variant) ---
84
+
85
+ private async getRedocSidebar(): Promise<WebdriverIO.Element> {
86
+ return $('.redoc-wrap > .menu-content');
87
+ }
88
+
89
+ private async getRedocProjectHeader(): Promise<WebdriverIO.Element> {
90
+ return (await this.getRedocSidebar()).$('div.redoc-project-header');
91
+ }
92
+
93
+ /** The Redoc spec-page's own doc-version-picker, or null if this spec has no versions. */
94
+ async getRedocPicker(): Promise<WebdriverIO.Element | null> {
95
+ const header = await this.getRedocProjectHeader();
96
+ const wrapper = await header.$('.redoc-version-picker');
97
+ if (!(await wrapper.isExisting())) {
98
+ return null;
99
+ }
100
+ return wrapper.$('doc-version-picker');
101
+ }
102
+
103
+ /**
104
+ * The dismissible "Newer Version Available" banner shown on a non-latest Redoc spec version.
105
+ * Reuses the `doc-phase` component, inserted at the top of `.api-content`. Returns null when
106
+ * absent (latest version, single version, or no versions).
107
+ */
108
+ async getRedocVersionBanner(): Promise<WebdriverIO.Element | null> {
109
+ const banner = await $(
110
+ 'doc-redoc-reference > .redoc-container .api-content .doc-phase-wrapper doc-phase',
111
+ );
112
+ return (await banner.isExisting()) ? banner : null;
113
+ }
114
+
115
+ /**
116
+ * The markdown-topic analog of `getRedocVersionBanner`. TODO: unconfirmed whether markdown
117
+ * topics render this banner at all yet — only the Redoc spec-page banner is confirmed shipped.
118
+ */
119
+ async getMarkdownVersionBanner(): Promise<WebdriverIO.Element | null> {
120
+ const banner = await $('>>>doc-content-layout .doc-phase-wrapper doc-phase');
121
+ return (await banner.isExisting()) ? banner : null;
122
+ }
123
+
124
+ async getBannerTitle(banner: WebdriverIO.Element): Promise<string> {
125
+ return (await banner.shadow$('.doc-phase-title')).getText();
126
+ }
127
+
128
+ async getBannerLinkHref(banner: WebdriverIO.Element): Promise<string> {
129
+ const link = await (await banner.shadow$('.doc-phase-body')).$('a');
130
+ return link.getAttribute('href');
131
+ }
132
+
133
+ // --- Generic picker interactions (work for either variant) ---
134
+
135
+ async getSelectedVersionLabel(picker: WebdriverIO.Element): Promise<string> {
136
+ const label = await picker.shadow$('p.selected-version-label');
137
+ return (await label.getText()).trim();
138
+ }
139
+
140
+ async isReadOnly(picker: WebdriverIO.Element): Promise<boolean> {
141
+ return (await picker.shadow$('p.version-picker-readonly')).isExisting();
142
+ }
143
+
144
+ /** Default (non-small) variant only — the redoc spec-page picker. */
145
+ async isLatestBadgeShown(picker: WebdriverIO.Element): Promise<boolean> {
146
+ return (await picker.shadow$('dx-type-badge.latest-badge')).isExisting();
147
+ }
148
+
149
+ async isNotLatestBadgeShown(picker: WebdriverIO.Element): Promise<boolean> {
150
+ return (await picker.shadow$('dx-type-badge.not-latest-badge')).isExisting();
151
+ }
152
+
153
+ /** Small variant only — the sidebar picker shows a colored dot instead of a badge. */
154
+ async isLatestDotShown(picker: WebdriverIO.Element): Promise<boolean> {
155
+ return (await picker.shadow$('span.version-picker-dot-latest')).isExisting();
156
+ }
157
+
158
+ async isNotLatestDotShown(picker: WebdriverIO.Element): Promise<boolean> {
159
+ return (await picker.shadow$('span.version-picker-dot-not-latest')).isExisting();
160
+ }
161
+
162
+ async openPicker(picker: WebdriverIO.Element): Promise<void> {
163
+ const trigger = await picker.shadow$('.version-picker-button');
164
+ await trigger.waitForDisplayed();
165
+ await trigger.waitForClickable();
166
+ await trigger.click();
167
+ }
168
+
169
+ async getOptionLabel(option: WebdriverIO.Element): Promise<string> {
170
+ const label = await option.shadow$('div>div');
171
+ return (await label.getText()).trim();
172
+ }
173
+
174
+ /** Opens the picker and clicks the option whose label matches exactly. */
175
+ async selectVersionByLabel(picker: WebdriverIO.Element, label: string): Promise<void> {
176
+ await this.openPicker(picker);
177
+ const options = await this.getVersionOptions();
178
+ for (const option of options) {
179
+ if ((await this.getOptionLabel(option)) === label) {
180
+ await option.waitForClickable();
181
+ await option.click();
182
+ return;
183
+ }
184
+ }
185
+ throw new Error(`No version option found with label "${label}"`);
186
+ }
187
+
188
+ async readSessionVersionSelections(): Promise<Record<string, string> | null> {
189
+ // The tsconfig's lib list omits "DOM", so sessionStorage isn't a known global here even
190
+ // though this callback runs in the browser — route through globalThis to sidestep that.
191
+ const raw = await browser.execute(() =>
192
+ (globalThis as any).sessionStorage.getItem('sfdocs-topic-version-selections'),
193
+ );
194
+ return raw ? JSON.parse(raw) : null;
195
+ }
196
+ }
@@ -1,6 +1,9 @@
1
+ import DocsPage from '../pageobjects/docs.page';
1
2
  import GuidePage from '../pageobjects/guide.page';
2
3
  import { ReferencePage } from '../pageobjects/reference.page';
3
4
 
5
+ const DOCS_MARKDOWN_PATH = '/docs/docs-content-type-example/get-started.html';
6
+
4
7
  const EN_US_GUIDE_PATH = '/docs/service/vscode-maintainer/guide/vscode-overview.html';
5
8
  const EN_US_MARKDOWN_REFERENCE_PATH =
6
9
  '/docs/commerce/flexible-references/references/shopper-manual-ref/quick-start.html';
@@ -41,6 +44,16 @@ describe('Content action toolbar scenarios', () => {
41
44
  await expectToolbarButtons(toolbar);
42
45
  });
43
46
 
47
+ it('Markdown docs page should have the toolbar', async () => {
48
+ const page = new DocsPage(DOCS_MARKDOWN_PATH);
49
+ page.open();
50
+ await page.waitPageLoad();
51
+
52
+ const toolbar = await $('>>>doc-content-action-toolbar');
53
+ await toolbar.waitForDisplayed();
54
+ await expectToolbarButtons(toolbar);
55
+ });
56
+
44
57
  it("Non-markdown reference shouldn't have toolbar", async () => {
45
58
  const referencePage = new ReferencePage(NON_MARKDOWN_REFERENCE_PATH);
46
59
  referencePage.open();