@salesforcedevs/sfdocs-e2e 1.0.12-fix-ver-picker-1 → 1.0.12-fix-redoc-topic-v-1
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/common-wdio.conf.js +8 -4
- package/package.json +1 -1
- package/src/content-repo/specs/componentSpecification.test.ts +2 -18
- package/src/content-repo/specs/sitemap.test.ts +3 -1
- package/src/shared/pageobjects/docs.page.ts +17 -63
- package/src/shared/pageobjects/page.ts +67 -0
- package/src/shared/pageobjects/reference.page.ts +0 -1
- package/src/shared/pageobjects/topic-version.page.ts +176 -0
- package/src/shared/specs/docsContentType.test.ts +7 -8
- package/src/shared/specs/guides.test.ts +19 -28
- package/src/shared/specs/redoc-reference.test.ts +1 -4
- package/src/shared/specs/reference.test.ts +11 -17
- package/src/shared/specs/topicVersioning.test.ts +502 -0
package/common-wdio.conf.js
CHANGED
|
@@ -89,10 +89,14 @@ const chrome = resolvedChromeDriverVersion ? { version: resolvedChromeDriverVers
|
|
|
89
89
|
const drivers = {
|
|
90
90
|
// ChromeDriver 115+ is distributed via Chrome for Testing:
|
|
91
91
|
// https://googlechromelabs.github.io/chrome-for-testing/
|
|
92
|
-
chrome
|
|
93
|
-
firefox: { version: '0.35.0' }, // https://github.com/mozilla/geckodriver/releases - 0.30.0+ supports ARM64 Mac
|
|
92
|
+
chrome
|
|
94
93
|
};
|
|
95
94
|
|
|
95
|
+
// selenium-standalone's own default (4.9.0) doesn't support the `-I` driver flag it
|
|
96
|
+
// unconditionally passes on Selenium 4.x, so the server exits immediately on start.
|
|
97
|
+
// Pinning a version that does support it (4.18.1) avoids that crash.
|
|
98
|
+
const seleniumServerVersion = process.env.SELENIUM_VERSION || '4.18.1';
|
|
99
|
+
|
|
96
100
|
exports.config = {
|
|
97
101
|
//
|
|
98
102
|
// ====================
|
|
@@ -188,8 +192,8 @@ exports.config = {
|
|
|
188
192
|
[
|
|
189
193
|
'selenium-standalone',
|
|
190
194
|
{
|
|
191
|
-
installArgs: { drivers }, // drivers to install
|
|
192
|
-
args: { drivers }, // drivers to use
|
|
195
|
+
installArgs: { version: seleniumServerVersion, drivers }, // drivers to install
|
|
196
|
+
args: { version: seleniumServerVersion, drivers }, // drivers to use
|
|
193
197
|
},
|
|
194
198
|
],
|
|
195
199
|
],
|
package/package.json
CHANGED
|
@@ -149,17 +149,9 @@ describe('Component Specification', () => {
|
|
|
149
149
|
'div.container div.sidebar-content dx-tree',
|
|
150
150
|
);
|
|
151
151
|
|
|
152
|
-
const getItemText = async (item: WebdriverIO.Element) => {
|
|
153
|
-
const link = await item.shadow$('a');
|
|
154
|
-
const tiles = await link.$$('dx-tree-tile');
|
|
155
|
-
const tileLink = await tiles[0].shadow$('a.tile.sidebar-item');
|
|
156
|
-
const label = await tileLink.$('span.tile-label');
|
|
157
|
-
return (await label.getText()).trim();
|
|
158
|
-
};
|
|
159
|
-
|
|
160
152
|
const firstItem = (await sidebarTrees[0].shadow$$('dx-tree-item'))[0];
|
|
161
153
|
const firstChildItem = (await firstItem.shadow$$('dx-tree-item'))[0];
|
|
162
|
-
const firstChildText = await
|
|
154
|
+
const firstChildText = await page.getTreeItemLabel(firstChildItem);
|
|
163
155
|
|
|
164
156
|
expect(firstChildText).toBe('Accordion');
|
|
165
157
|
});
|
|
@@ -264,18 +256,10 @@ describe('Component Specification', () => {
|
|
|
264
256
|
'div.container div.sidebar-content dx-tree',
|
|
265
257
|
);
|
|
266
258
|
|
|
267
|
-
const getItemText = async (item: WebdriverIO.Element) => {
|
|
268
|
-
const link = await item.shadow$('a');
|
|
269
|
-
const tiles = await link.$$('dx-tree-tile');
|
|
270
|
-
const tileLink = await tiles[0].shadow$('a.tile.sidebar-item');
|
|
271
|
-
const label = await tileLink.$('span.tile-label');
|
|
272
|
-
return (await label.getText()).trim();
|
|
273
|
-
};
|
|
274
|
-
|
|
275
259
|
const auraItem = (await sidebarTrees[1].shadow$$('dx-tree-item'))[0];
|
|
276
260
|
const lightningItem = (await auraItem.shadow$$('dx-tree-item'))[0];
|
|
277
261
|
const componentsItem = (await lightningItem.shadow$$('dx-tree-item'))[0];
|
|
278
|
-
const firstComponentText = await
|
|
262
|
+
const firstComponentText = await page.getTreeItemLabel(
|
|
279
263
|
(
|
|
280
264
|
await componentsItem.shadow$$('dx-tree-item')
|
|
281
265
|
)[0],
|
|
@@ -56,7 +56,9 @@ describe('Test Sitemap Routes', () => {
|
|
|
56
56
|
);
|
|
57
57
|
|
|
58
58
|
// Spec (Redoc) topics are published without an extension.
|
|
59
|
-
expect(data.body).toContain(
|
|
59
|
+
expect(data.body).toContain(
|
|
60
|
+
'<loc>/docs/docs-content-type-example/widgets-api-reference</loc>',
|
|
61
|
+
);
|
|
60
62
|
expect(data.body).toContain('<loc>/docs/docs-content-type-example/new-sample-api</loc>');
|
|
61
63
|
|
|
62
64
|
// Each markdown URL appears exactly once.
|
|
@@ -15,77 +15,23 @@ class DocsPage extends Page {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
async getContentHeading(): Promise<WebdriverIO.Element> {
|
|
19
|
-
return (await $('doc-unified-content-layout')).$('div.markdown-content h1');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private async getSidebar(): Promise<WebdriverIO.Element> {
|
|
23
|
-
return (await $('>>>doc-content-layout')).shadow$('div > dx-sidebar-old');
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async waitForSidebar(): Promise<void> {
|
|
27
|
-
await (await this.getSidebar()).waitForDisplayed();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async getSidebarTrees(): Promise<WebdriverIO.ElementArray> {
|
|
31
|
-
const sidebar = await this.getSidebar();
|
|
32
|
-
return sidebar.shadow$$('div.container div.sidebar-content dx-tree');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private async getTreeTile(treeItem: WebdriverIO.Element): Promise<WebdriverIO.Element> {
|
|
36
|
-
await treeItem.waitForExist();
|
|
37
|
-
return treeItem.shadow$('a > dx-tree-tile');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
private async getTreeItemLabel(treeItem: WebdriverIO.Element): Promise<string> {
|
|
41
|
-
const treeTile = await this.getTreeTile(treeItem);
|
|
42
|
-
await treeTile.waitForExist();
|
|
43
|
-
const label = await treeTile.shadow$('span.tile-label');
|
|
44
|
-
return (await label.getText()).trim();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private async findTreeItemByLabel(
|
|
48
|
-
treeItem: WebdriverIO.Element,
|
|
49
|
-
label: string,
|
|
50
|
-
): Promise<WebdriverIO.Element | null> {
|
|
51
|
-
if ((await this.getTreeItemLabel(treeItem)) === label) {
|
|
52
|
-
return treeItem;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const children = await treeItem.shadow$$('dx-tree-item');
|
|
56
|
-
for (const child of children) {
|
|
57
|
-
const match = await this.findTreeItemByLabel(child, label);
|
|
58
|
-
if (match) {
|
|
59
|
-
return match;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
18
|
async getSidebarTile(label: string): Promise<WebdriverIO.Element> {
|
|
66
|
-
|
|
67
|
-
for (const tree of trees) {
|
|
68
|
-
const rootItem = await tree.shadow$('dx-tree-item');
|
|
69
|
-
const match = await this.findTreeItemByLabel(rootItem, label);
|
|
70
|
-
if (match) {
|
|
71
|
-
return match;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
throw new Error(`No sidebar tile found with label "${label}"`);
|
|
19
|
+
return this.getSidebarTreeItemByLabel(label);
|
|
75
20
|
}
|
|
76
21
|
|
|
77
22
|
async getTileForwardArrow(label: string): Promise<WebdriverIO.Element> {
|
|
78
23
|
const treeItem = await this.getSidebarTile(label);
|
|
79
|
-
|
|
80
|
-
await treeTile.waitForExist();
|
|
81
|
-
return treeTile.shadow$('.tile-forward-arrow');
|
|
24
|
+
return (await this.getTreeItemTile(treeItem)).shadow$('.tile-forward-arrow');
|
|
82
25
|
}
|
|
83
26
|
|
|
84
27
|
async getTileExternalLinkIcon(label: string): Promise<WebdriverIO.Element> {
|
|
85
28
|
const treeItem = await this.getSidebarTile(label);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
29
|
+
return (await this.getTreeItemTile(treeItem)).shadow$('.tile-external-link-icon');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async getTileAnchor(label: string): Promise<WebdriverIO.Element> {
|
|
33
|
+
const treeItem = await this.getSidebarTile(label);
|
|
34
|
+
return (await this.getTreeItemTile(treeItem)).shadow$('a');
|
|
89
35
|
}
|
|
90
36
|
|
|
91
37
|
async getRedocBackLink(): Promise<WebdriverIO.Element> {
|
|
@@ -119,8 +65,16 @@ class DocsPage extends Page {
|
|
|
119
65
|
}
|
|
120
66
|
|
|
121
67
|
async selectLanguage(displayText: string): Promise<void> {
|
|
68
|
+
const picker = await this.getLanguagePicker();
|
|
69
|
+
// A hidden mobile/desktop duplicate of the footer nav can overlap this button at the
|
|
70
|
+
// same screen coordinates, so a native pointer click risks getting intercepted by it.
|
|
71
|
+
await browser.execute((el: { click: () => void }) => el.click(), picker);
|
|
72
|
+
|
|
122
73
|
const options = await $$('>>>dx-dropdown-option');
|
|
123
|
-
|
|
74
|
+
const isDisplayed = await Promise.all(options.map((option) => option.isDisplayed()));
|
|
75
|
+
const visibleOptions = options.filter((_, index) => isDisplayed[index]);
|
|
76
|
+
|
|
77
|
+
for (const option of visibleOptions) {
|
|
124
78
|
const link = await option.shadow$('a.option');
|
|
125
79
|
if ((await link.getText()).trim() === displayText) {
|
|
126
80
|
await browser.execute((el: { click: () => void }) => el.click(), link);
|
|
@@ -58,6 +58,10 @@ export default class Page {
|
|
|
58
58
|
).waitForDisplayed({ timeout: this.REFERENCE_LOAD_WAIT_TIME });
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
async getContentHeading(): Promise<WebdriverIO.Element> {
|
|
62
|
+
return (await $('doc-unified-content-layout')).$('div.markdown-content h1');
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
async waitForGuidePageLoad(): Promise<void> {
|
|
62
66
|
await $('doc-content-layout').waitForDisplayed({ timeout: 5000 });
|
|
63
67
|
}
|
|
@@ -84,6 +88,69 @@ export default class Page {
|
|
|
84
88
|
return await $('>>>doc-specification-content');
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
async getTreeItemTile(treeItem: WebdriverIO.Element): Promise<WebdriverIO.Element> {
|
|
92
|
+
return treeItem.shadow$('dx-tree-tile');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async getSidebar(): Promise<WebdriverIO.Element> {
|
|
96
|
+
return (await $('>>>doc-content-layout')).shadow$('div > dx-sidebar-old');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async waitForSidebar(): Promise<void> {
|
|
100
|
+
await (await this.getSidebar()).waitForDisplayed();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getSidebarTrees(): Promise<WebdriverIO.ElementArray> {
|
|
104
|
+
const sidebar = await this.getSidebar();
|
|
105
|
+
return sidebar.shadow$$('div.container div.sidebar-content dx-tree');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getTreeItemLabel(treeItem: WebdriverIO.Element): Promise<string> {
|
|
109
|
+
const tile = await this.getTreeItemTile(treeItem);
|
|
110
|
+
const label = await tile.shadow$('span.tile-label');
|
|
111
|
+
return (await label.getText()).trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async findTreeItemByLabel(
|
|
115
|
+
treeItem: WebdriverIO.Element,
|
|
116
|
+
label: string,
|
|
117
|
+
): Promise<WebdriverIO.Element | null> {
|
|
118
|
+
if ((await this.getTreeItemLabel(treeItem)) === label) {
|
|
119
|
+
return treeItem;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const children = await treeItem.shadow$$('dx-tree-item');
|
|
123
|
+
for (const child of children) {
|
|
124
|
+
const match = await this.findTreeItemByLabel(child, label);
|
|
125
|
+
if (match) {
|
|
126
|
+
return match;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getSidebarTreeItemByLabel(label: string): Promise<WebdriverIO.Element> {
|
|
133
|
+
const trees = await this.getSidebarTrees();
|
|
134
|
+
for (const tree of trees) {
|
|
135
|
+
const rootItem = await tree.shadow$('dx-tree-item');
|
|
136
|
+
const match = await this.findTreeItemByLabel(rootItem, label);
|
|
137
|
+
if (match) {
|
|
138
|
+
return match;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`No sidebar tree item found with label "${label}"`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Options render in a single shared overlay, one per picker on the page, hidden until their
|
|
146
|
+
* own dropdown opens — filtering to visible ones scopes this to whichever just opened.
|
|
147
|
+
*/
|
|
148
|
+
async getVersionOptions(): Promise<WebdriverIO.Element[]> {
|
|
149
|
+
const allOptions = await $$('>>>doc-version-picker dx-dropdown-option');
|
|
150
|
+
const isDisplayed = await Promise.all(allOptions.map((option) => option.isDisplayed()));
|
|
151
|
+
return allOptions.filter((_, index) => isDisplayed[index]);
|
|
152
|
+
}
|
|
153
|
+
|
|
87
154
|
async waitForSpinnerToDisappear(element: WebdriverIO.Element): Promise<void> {
|
|
88
155
|
await browser.waitUntil(
|
|
89
156
|
async function () {
|
|
@@ -28,7 +28,6 @@ export class ReferencePage extends Page {
|
|
|
28
28
|
const versionPicker = await this.getVersionPickerBtn();
|
|
29
29
|
await versionPicker.click();
|
|
30
30
|
const versionOptions = await $$('>>>doc-version-picker dx-dropdown-option');
|
|
31
|
-
await versionOptions[1].waitForClickable();
|
|
32
31
|
await versionOptions[1].click();
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import Page from './page';
|
|
2
|
+
|
|
3
|
+
/** Must match the sessionStorage key the doc-version-picker component writes to. */
|
|
4
|
+
const TOPIC_VERSION_SELECTIONS_SESSION_KEY = 'sfdocs-topic-version-selections';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Page object for topic-level (per-topic) version picker interactions.
|
|
8
|
+
*
|
|
9
|
+
* Two `doc-version-picker` instances exist for a versioned topic:
|
|
10
|
+
* - the SIDEBAR picker (small variant), rendered as a sibling of `dx-tree-tile` inside the
|
|
11
|
+
* versioned tree row's own shadow root (`class="tree-item-version-picker"`).
|
|
12
|
+
* - the REDOC SPEC-PAGE picker (default variant), rendered inside `div.redoc-project-header`
|
|
13
|
+
* alongside the back-link/spec title. Redoc mounts this header into BOTH `.menu-content` and
|
|
14
|
+
* `.api-content`, so we always scope through the sidebar's copy (`getRedocProjectHeader`) to
|
|
15
|
+
* avoid matching the duplicate.
|
|
16
|
+
*/
|
|
17
|
+
export default class TopicVersionPage extends Page {
|
|
18
|
+
async waitForMarkdownTopicLoad(): Promise<void> {
|
|
19
|
+
await (
|
|
20
|
+
await $('>>>doc-content-layout')
|
|
21
|
+
).waitForDisplayed({ timeout: this.REFERENCE_LOAD_WAIT_TIME });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async waitForRedocTopicLoad(): Promise<void> {
|
|
25
|
+
await $('doc-redoc-reference > .redoc-container').waitForDisplayed({
|
|
26
|
+
timeout: this.REFERENCE_LOAD_WAIT_TIME,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async getSidebarTreeItem(label: string): Promise<WebdriverIO.Element> {
|
|
31
|
+
return this.getSidebarTreeItemByLabel(label);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The sidebar row's own doc-version-picker (small variant), or null if it has none. */
|
|
35
|
+
async getSidebarPicker(rowLabel: string): Promise<WebdriverIO.Element | null> {
|
|
36
|
+
const treeItem = await this.getSidebarTreeItem(rowLabel);
|
|
37
|
+
const picker = await treeItem.shadow$('.tree-item-version-picker');
|
|
38
|
+
return (await picker.isExisting()) ? picker : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Redoc spec-page project header (default variant) ---
|
|
42
|
+
|
|
43
|
+
private async getRedocSidebar(): Promise<WebdriverIO.Element> {
|
|
44
|
+
return $('.redoc-wrap > .menu-content');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async getRedocProjectHeader(): Promise<WebdriverIO.Element> {
|
|
48
|
+
return (await this.getRedocSidebar()).$('div.redoc-project-header');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Back link in the menu header, not the duplicate inside api-content. */
|
|
52
|
+
async getRedocBackLink(): Promise<WebdriverIO.Element> {
|
|
53
|
+
return (await this.getRedocProjectHeader()).$('a.redoc-project-back');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The Redoc spec-page's own doc-version-picker, or null if this spec has no versions. */
|
|
57
|
+
async getRedocPicker(): Promise<WebdriverIO.Element | null> {
|
|
58
|
+
const header = await this.getRedocProjectHeader();
|
|
59
|
+
const wrapper = await header.$('.redoc-version-picker');
|
|
60
|
+
if (!(await wrapper.isExisting())) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return wrapper.$('doc-version-picker');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The dismissible "Newer Version Available" banner shown on a non-latest Redoc spec version.
|
|
68
|
+
* Reuses the `doc-phase` component, inserted at the top of `.api-content`. Returns null when
|
|
69
|
+
* absent (latest version, single version, or no versions).
|
|
70
|
+
*/
|
|
71
|
+
async getRedocVersionBanner(): Promise<WebdriverIO.Element | null> {
|
|
72
|
+
const banner = await $(
|
|
73
|
+
'doc-redoc-reference > .redoc-container .api-content .doc-phase-wrapper doc-phase',
|
|
74
|
+
);
|
|
75
|
+
return (await banner.isExisting()) ? banner : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The markdown-topic analog of `getRedocVersionBanner`.
|
|
80
|
+
*/
|
|
81
|
+
async getMarkdownVersionBanner(): Promise<WebdriverIO.Element | null> {
|
|
82
|
+
const banner = await $('>>>doc-content-layout doc-phase[slot="version-banner"]');
|
|
83
|
+
return (await banner.isExisting()) ? banner : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getBannerTitle(banner: WebdriverIO.Element): Promise<string> {
|
|
87
|
+
return (await banner.shadow$('.doc-phase-title')).getText();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getBannerLinkHref(banner: WebdriverIO.Element): Promise<string> {
|
|
91
|
+
const link = await (await banner.shadow$('.doc-phase-body')).$('a');
|
|
92
|
+
return link.getAttribute('href');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- Generic picker interactions (work for either variant) ---
|
|
96
|
+
|
|
97
|
+
async getSelectedVersionLabel(picker: WebdriverIO.Element): Promise<string> {
|
|
98
|
+
const label = await picker.shadow$('p.selected-version-label');
|
|
99
|
+
return (await label.getText()).trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async isReadOnly(picker: WebdriverIO.Element): Promise<boolean> {
|
|
103
|
+
return (await picker.shadow$('p.version-picker-readonly')).isExisting();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Default (non-small) variant only — the redoc spec-page picker. */
|
|
107
|
+
async isLatestBadgeShown(picker: WebdriverIO.Element): Promise<boolean> {
|
|
108
|
+
return (await picker.shadow$('dx-type-badge.latest-badge')).isExisting();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async isNotLatestBadgeShown(picker: WebdriverIO.Element): Promise<boolean> {
|
|
112
|
+
return (await picker.shadow$('dx-type-badge.not-latest-badge')).isExisting();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Small variant only — the sidebar picker shows a colored dot instead of a badge. */
|
|
116
|
+
async isLatestDotShown(picker: WebdriverIO.Element): Promise<boolean> {
|
|
117
|
+
return (await picker.shadow$('span.version-picker-dot-latest')).isExisting();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async isNotLatestDotShown(picker: WebdriverIO.Element): Promise<boolean> {
|
|
121
|
+
return (await picker.shadow$('span.version-picker-dot-not-latest')).isExisting();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async openPicker(picker: WebdriverIO.Element): Promise<void> {
|
|
125
|
+
const trigger = await picker.shadow$('.version-picker-button');
|
|
126
|
+
await trigger.waitForDisplayed();
|
|
127
|
+
await trigger.waitForClickable();
|
|
128
|
+
await trigger.click();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async getOptionLabel(option: WebdriverIO.Element): Promise<string> {
|
|
132
|
+
const label = await option.shadow$('div>div');
|
|
133
|
+
return (await label.getText()).trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async selectVersionByLabel(picker: WebdriverIO.Element, label: string): Promise<void> {
|
|
137
|
+
await this.openPicker(picker);
|
|
138
|
+
const options = await this.getVersionOptions();
|
|
139
|
+
for (const option of options) {
|
|
140
|
+
if ((await this.getOptionLabel(option)) === label) {
|
|
141
|
+
await option.waitForClickable();
|
|
142
|
+
await option.click();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`No version option found with label "${label}"`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async waitForUrlCondition(
|
|
150
|
+
description: string,
|
|
151
|
+
predicate: (url: string) => boolean,
|
|
152
|
+
): Promise<void> {
|
|
153
|
+
await browser.waitUntil(async () => predicate(await browser.getUrl()), {
|
|
154
|
+
timeout: 15000,
|
|
155
|
+
timeoutMsg: `expected URL to ${description}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async waitForUrlToContain(path: string): Promise<void> {
|
|
160
|
+
await this.waitForUrlCondition(`contain ${path}`, (url) => url.includes(path));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async waitForUrlToEndWith(path: string): Promise<void> {
|
|
164
|
+
await this.waitForUrlCondition(`end with ${path}`, (url) => url.endsWith(path));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async readSessionVersionSelections(): Promise<Record<string, string> | null> {
|
|
168
|
+
// The tsconfig's lib list omits "DOM", so sessionStorage isn't a known global here even
|
|
169
|
+
// though this callback runs in the browser — route through globalThis to sidestep that.
|
|
170
|
+
const raw = await browser.execute(
|
|
171
|
+
(key) => (globalThis as any).sessionStorage.getItem(key),
|
|
172
|
+
TOPIC_VERSION_SELECTIONS_SESSION_KEY,
|
|
173
|
+
);
|
|
174
|
+
return raw ? JSON.parse(raw) : null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -85,7 +85,7 @@ describe('Docs content type', () => {
|
|
|
85
85
|
await parentTile.click();
|
|
86
86
|
await page.waitPageLoad();
|
|
87
87
|
|
|
88
|
-
expect(await browser.getUrl()).toContain('
|
|
88
|
+
expect(await browser.getUrl()).toContain('errors-and-rate-limits.html');
|
|
89
89
|
expect(await (await page.getContentHeading()).getText()).toContain(
|
|
90
90
|
'Errors and Rate Limits',
|
|
91
91
|
);
|
|
@@ -197,7 +197,7 @@ describe('Docs content type > internal links', () => {
|
|
|
197
197
|
|
|
198
198
|
describe('Docs content type > spec (Redoc) topic', () => {
|
|
199
199
|
beforeEach(async () => {
|
|
200
|
-
page = new DocsPage('/docs/docs-content-type-example/
|
|
200
|
+
page = new DocsPage('/docs/docs-content-type-example/widgets-api-reference');
|
|
201
201
|
page.open();
|
|
202
202
|
await page.waitForRedocLoad();
|
|
203
203
|
});
|
|
@@ -218,7 +218,7 @@ describe('Docs content type > spec (Redoc) topic', () => {
|
|
|
218
218
|
|
|
219
219
|
it('falls back to the landing topic when the back link is clicked on a directly opened spec', async () => {
|
|
220
220
|
await browser.reloadSession();
|
|
221
|
-
page = new DocsPage('/docs/docs-content-type-example/
|
|
221
|
+
page = new DocsPage('/docs/docs-content-type-example/widgets-api-reference');
|
|
222
222
|
page.open();
|
|
223
223
|
await page.waitForRedocLoad();
|
|
224
224
|
|
|
@@ -245,7 +245,7 @@ describe('Docs content type > locale picker', () => {
|
|
|
245
245
|
});
|
|
246
246
|
|
|
247
247
|
it('shows the language picker on a spec topic of a localized project', async () => {
|
|
248
|
-
page = new DocsPage('/docs/docs-content-type-example/
|
|
248
|
+
page = new DocsPage('/docs/docs-content-type-example/widgets-api-reference');
|
|
249
249
|
page.open();
|
|
250
250
|
await page.waitForRedocLoad();
|
|
251
251
|
|
|
@@ -256,7 +256,7 @@ describe('Docs content type > locale picker', () => {
|
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
it('re-renders the spec in the selected locale when switching language on a spec page', async () => {
|
|
259
|
-
page = new DocsPage('/docs/docs-content-type-example/
|
|
259
|
+
page = new DocsPage('/docs/docs-content-type-example/widgets-api-reference');
|
|
260
260
|
page.open();
|
|
261
261
|
await page.waitForRedocLoad();
|
|
262
262
|
|
|
@@ -306,7 +306,7 @@ describe('Docs content type > localized (ja-jp) rendering', () => {
|
|
|
306
306
|
});
|
|
307
307
|
|
|
308
308
|
it('renders the Redoc view from a ja-jp spec URL', async () => {
|
|
309
|
-
page = new DocsPage('/docs/ja-jp/docs-content-type-example/
|
|
309
|
+
page = new DocsPage('/docs/ja-jp/docs-content-type-example/widgets-api-reference');
|
|
310
310
|
page.open();
|
|
311
311
|
await page.waitForRedocLoad();
|
|
312
312
|
|
|
@@ -355,8 +355,7 @@ describe('Docs content type > external link', () => {
|
|
|
355
355
|
await page.waitPageLoad();
|
|
356
356
|
await page.waitForSidebar();
|
|
357
357
|
|
|
358
|
-
const
|
|
359
|
-
const anchor = await tile.shadow$('a');
|
|
358
|
+
const anchor = await page.getTileAnchor('Salesforce Developers');
|
|
360
359
|
expect(await anchor.getAttribute('href')).toContain('https://developer.salesforce.com');
|
|
361
360
|
expect(await anchor.getAttribute('target')).toBe('_blank');
|
|
362
361
|
});
|
|
@@ -66,24 +66,24 @@ describe('Test Guides page components functionality> ', () => {
|
|
|
66
66
|
|
|
67
67
|
expect(sideBarContent.length).toEqual(6);
|
|
68
68
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
expect(
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(
|
|
80
|
-
|
|
81
|
-
await
|
|
69
|
+
const nextStepOption = await (
|
|
70
|
+
await sideBarContent[3].shadow$('dx-tree-item')
|
|
71
|
+
).shadow$('dx-tree-tile');
|
|
72
|
+
|
|
73
|
+
expect(nextStepOption.isExisting()).toBeTruthy();
|
|
74
|
+
|
|
75
|
+
const installVSCodeOptionTile = await (
|
|
76
|
+
await sideBarContent[1].shadow$('dx-tree-item')
|
|
77
|
+
).shadow$('dx-tree-tile');
|
|
78
|
+
|
|
79
|
+
expect(installVSCodeOptionTile.isExisting()).toBeTruthy();
|
|
80
|
+
|
|
81
|
+
await (await sideBarContent[1].shadow$('dx-tree-item')).click();
|
|
82
82
|
await page.waitPageLoad();
|
|
83
83
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const installVSCodeOption = await (
|
|
85
|
+
await sideBarContent[1].shadow$('dx-tree-item')
|
|
86
|
+
).shadow$$('dx-tree-item');
|
|
87
87
|
|
|
88
88
|
expect(installVSCodeOption.length).toEqual(3);
|
|
89
89
|
|
|
@@ -304,15 +304,9 @@ describe('Search guides', () => {
|
|
|
304
304
|
await browser.keys('SFRA');
|
|
305
305
|
|
|
306
306
|
const trees = await sidebar.shadow$$('div.container div.sidebar-content dx-tree');
|
|
307
|
-
const
|
|
308
|
-
await rootTreeItem.waitForExist();
|
|
309
|
-
const servicesTree = await rootTreeItem.shadow$('dx-tree-item');
|
|
310
|
-
await servicesTree.waitForExist();
|
|
307
|
+
const servicesTree = await (await trees[0].shadow$('dx-tree-item')).shadow$('dx-tree-item');
|
|
311
308
|
const treeItem = await servicesTree.shadow$('dx-tree-item');
|
|
312
|
-
await treeItem.
|
|
313
|
-
const treeTile = await treeItem.shadow$('dx-tree-tile');
|
|
314
|
-
await treeTile.waitForExist();
|
|
315
|
-
const item = await treeTile.shadow$('span');
|
|
309
|
+
const item = await (await treeItem.shadow$('dx-tree-tile')).shadow$('span');
|
|
316
310
|
const itemText = await item.getText();
|
|
317
311
|
expect(itemText).toBe('Find SFRA Tools');
|
|
318
312
|
});
|
|
@@ -328,10 +322,7 @@ describe('Search guides', () => {
|
|
|
328
322
|
await browser.keys('SFRA');
|
|
329
323
|
|
|
330
324
|
const trees = await (await sidebar).shadow$$('div.container div.sidebar-content dx-tree');
|
|
331
|
-
const
|
|
332
|
-
await rootTreeItem.waitForExist();
|
|
333
|
-
const servicesTree = await rootTreeItem.shadow$('dx-tree-item');
|
|
334
|
-
await servicesTree.waitForExist();
|
|
325
|
+
const servicesTree = await (await trees[0].shadow$('dx-tree-item')).shadow$('dx-tree-item');
|
|
335
326
|
const treeItem = await servicesTree.shadow$('dx-tree-item');
|
|
336
327
|
await treeItem.click();
|
|
337
328
|
await page.waitPageLoad();
|
|
@@ -190,13 +190,10 @@ describe('Redoc API Reference page', () => {
|
|
|
190
190
|
|
|
191
191
|
const secondItem = sideBarContent[1];
|
|
192
192
|
const treeItem = await secondItem.shadow$('dx-tree-item');
|
|
193
|
-
await
|
|
194
|
-
const treeTile = await treeItem.shadow$('a > dx-tree-tile');
|
|
195
|
-
await treeTile.waitForExist();
|
|
193
|
+
const treeTile = await refPage.getTreeItemTile(treeItem);
|
|
196
194
|
const arrowIcon = await treeTile.shadow$(
|
|
197
195
|
'a > div[role="heading"] > div.tile-forward-arrow',
|
|
198
196
|
);
|
|
199
|
-
await arrowIcon.waitForExist();
|
|
200
197
|
|
|
201
198
|
expect(await arrowIcon.isExisting()).toBeTruthy();
|
|
202
199
|
});
|
|
@@ -106,11 +106,9 @@ describe('Reference Versioning', () => {
|
|
|
106
106
|
const versionBtn = await $('>>>.version-picker-button');
|
|
107
107
|
await versionBtn.waitForDisplayed();
|
|
108
108
|
await versionBtn.click();
|
|
109
|
-
const versionOptions = await
|
|
109
|
+
const versionOptions = await referencePage.getVersionOptions();
|
|
110
110
|
|
|
111
111
|
expect(versionOptions.length).toEqual(2);
|
|
112
|
-
await versionOptions[0].waitForClickable();
|
|
113
|
-
await versionOptions[1].waitForClickable();
|
|
114
112
|
expect(await (await versionOptions[0].shadow$('div>div')).getText()).toEqual('0.1.3');
|
|
115
113
|
expect(await (await versionOptions[1].shadow$('div>div')).getText()).toEqual('0.1.2');
|
|
116
114
|
});
|
|
@@ -120,20 +118,19 @@ describe('Reference Versioning', () => {
|
|
|
120
118
|
const searchBox = await dxInput.shadow$('input');
|
|
121
119
|
|
|
122
120
|
await searchBox.click();
|
|
123
|
-
await browser.keys('
|
|
121
|
+
await browser.keys('coupon');
|
|
122
|
+
await browser.pause(1000);
|
|
124
123
|
|
|
125
124
|
const dxTreeItems = await $$('>>>dx-tree-item');
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await (await dxTreeItems[2]).click();
|
|
126
|
+
await dxTreeItems[2].click();
|
|
130
127
|
|
|
131
128
|
const versionBtn = await $('>>>.version-picker-button');
|
|
132
129
|
|
|
133
130
|
await versionBtn.waitForDisplayed();
|
|
134
131
|
await versionBtn.click();
|
|
135
132
|
|
|
136
|
-
const versionOptions = await
|
|
133
|
+
const versionOptions = await referencePage.getVersionOptions();
|
|
137
134
|
|
|
138
135
|
await versionOptions[1].waitForClickable();
|
|
139
136
|
await versionOptions[1].click();
|
|
@@ -152,9 +149,7 @@ describe('Reference Versioning', () => {
|
|
|
152
149
|
await versionPicker.waitForDisplayed();
|
|
153
150
|
await versionPicker.click();
|
|
154
151
|
|
|
155
|
-
const versionOptions = await
|
|
156
|
-
|
|
157
|
-
await versionOptions[1].waitForClickable();
|
|
152
|
+
const versionOptions = await referencePage.getVersionOptions();
|
|
158
153
|
const selectedVersionText = await (await versionOptions[1].shadow$('div>div')).getText();
|
|
159
154
|
|
|
160
155
|
await versionOptions[1].click();
|
|
@@ -226,12 +221,11 @@ describe('Markdown-Based Flexible References', () => {
|
|
|
226
221
|
const sidebar = await (await $('>>>doc-content-layout')).shadow$('div > dx-sidebar-old');
|
|
227
222
|
const sideBarContent = await sidebar.shadow$$('div.container div.sidebar-content dx-tree');
|
|
228
223
|
expect(sideBarContent.length).toEqual(1);
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
await treeItem.click();
|
|
224
|
+
const tile = await (
|
|
225
|
+
await sideBarContent[0].shadow$('dx-tree-item')
|
|
226
|
+
).shadow$('dx-tree-tile');
|
|
227
|
+
expect(tile.isExisting()).toBeTruthy();
|
|
228
|
+
await (await sideBarContent[0].shadow$('dx-tree-item')).click();
|
|
235
229
|
await referencePage.waitForMarkdownBasedReferencePageLoad();
|
|
236
230
|
const title = await $('>>>h1');
|
|
237
231
|
const titleText = await title.getText();
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { it } from 'mocha';
|
|
2
|
+
import TopicVersionPage from '../pageobjects/topic-version.page';
|
|
3
|
+
|
|
4
|
+
// Fixtures live in the `docs-content-type-example` project, kept in sync between this monorepo's
|
|
5
|
+
// local `packages/example-repository` and the remote `salesforcedocs/example-repository`'s
|
|
6
|
+
// `mh/topic-level-version` branch (pinned in example-portal/docsportal.config.js).
|
|
7
|
+
const BASE = '/docs/docs-content-type-example';
|
|
8
|
+
|
|
9
|
+
// Version labels for each fixture's picker, named so a future relabel only needs updating here.
|
|
10
|
+
const OAUTH_V1_LABEL = "SU'26";
|
|
11
|
+
const OAUTH_V2_LABEL = "Winter '26 (API version 65.0) and Spring '26 (API version 66.0)";
|
|
12
|
+
const ERRORS_V2_LABEL = "Summer '26 (API version 67.0)";
|
|
13
|
+
const ERRORS_V1_LABEL = "Spring '26 (API version 66.0)";
|
|
14
|
+
const CLIENT_SDKS_V2_LABEL = "WI'26";
|
|
15
|
+
const CLIENT_SDKS_V1_LABEL = "SU'26";
|
|
16
|
+
const WIDGETS_API_REFERENCE_V2_LABEL = "SU'26";
|
|
17
|
+
const WIDGETS_API_REFERENCE_V1_LABEL = "SP'26";
|
|
18
|
+
|
|
19
|
+
let page: TopicVersionPage;
|
|
20
|
+
|
|
21
|
+
describe('Topic-level versioning > versioned markdown topic', () => {
|
|
22
|
+
// fixture: OAuth — latest is v2 (OAUTH_V2_LABEL), older is v1 (OAUTH_V1_LABEL).
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
page = new TopicVersionPage(`${BASE}/oauth.html`);
|
|
25
|
+
page.open();
|
|
26
|
+
await page.waitForMarkdownTopicLoad();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should render the picker and default to the latest version', async () => {
|
|
30
|
+
const picker = await page.getSidebarPicker('OAuth');
|
|
31
|
+
expect(picker).not.toBeNull();
|
|
32
|
+
expect(await page.getSelectedVersionLabel(picker!)).toEqual(OAUTH_V2_LABEL);
|
|
33
|
+
expect(await page.isLatestDotShown(picker!)).toBeTruthy();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should list all versions in declared order when the picker is opened', async () => {
|
|
37
|
+
const picker = await page.getSidebarPicker('OAuth');
|
|
38
|
+
await page.openPicker(picker!);
|
|
39
|
+
|
|
40
|
+
const options = await page.getVersionOptions();
|
|
41
|
+
expect(options.length).toEqual(2);
|
|
42
|
+
expect(await page.getOptionLabel(options[0])).toEqual(OAUTH_V2_LABEL);
|
|
43
|
+
expect(await page.getOptionLabel(options[1])).toEqual(OAUTH_V1_LABEL);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should switch the page content and URL when a non-latest version is selected', async () => {
|
|
47
|
+
const picker = await page.getSidebarPicker('OAuth');
|
|
48
|
+
await page.selectVersionByLabel(picker!, OAUTH_V1_LABEL);
|
|
49
|
+
await page.waitForMarkdownTopicLoad();
|
|
50
|
+
|
|
51
|
+
expect(await (await page.getContentHeading()).getText()).toEqual('OAuth 1.0');
|
|
52
|
+
await page.waitForUrlToContain(`${BASE}/v1/oauth.html`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return to the canonical URL when the latest version is reselected', async () => {
|
|
56
|
+
page = new TopicVersionPage(`${BASE}/v1/oauth.html`);
|
|
57
|
+
page.open();
|
|
58
|
+
await page.waitForMarkdownTopicLoad();
|
|
59
|
+
|
|
60
|
+
const picker = await page.getSidebarPicker('OAuth');
|
|
61
|
+
await page.selectVersionByLabel(picker!, OAUTH_V2_LABEL);
|
|
62
|
+
await page.waitForMarkdownTopicLoad();
|
|
63
|
+
|
|
64
|
+
await page.waitForUrlToEndWith(`${BASE}/oauth.html`);
|
|
65
|
+
const url = await browser.getUrl();
|
|
66
|
+
expect(url.endsWith(`${BASE}/oauth.html`)).toBeTruthy();
|
|
67
|
+
expect(url).not.toContain('/v2/');
|
|
68
|
+
expect(await (await page.getContentHeading()).getText()).toEqual('OAuth 2.0');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should show the "Newer Version Available" banner when the version is not latest', async () => {
|
|
72
|
+
page = new TopicVersionPage(`${BASE}/v1/oauth.html`);
|
|
73
|
+
page.open();
|
|
74
|
+
await page.waitForMarkdownTopicLoad();
|
|
75
|
+
|
|
76
|
+
const banner = await page.getMarkdownVersionBanner();
|
|
77
|
+
expect(banner).not.toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should not show the "Newer Version Available" banner when the version is latest', async () => {
|
|
81
|
+
const banner = await page.getMarkdownVersionBanner();
|
|
82
|
+
expect(banner).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Topic-level versioning > versioned parent with versioned child subtree', () => {
|
|
87
|
+
// fixture: Client SDKs — latest is v2 (CLIENT_SDKS_V2_LABEL), older is v1 (CLIENT_SDKS_V1_LABEL).
|
|
88
|
+
beforeEach(async () => {
|
|
89
|
+
page = new TopicVersionPage(`${BASE}/android.html`);
|
|
90
|
+
page.open();
|
|
91
|
+
await page.waitForMarkdownTopicLoad();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should render the picker on the parent and default to the latest version', async () => {
|
|
95
|
+
const picker = await page.getSidebarPicker('Client SDKs');
|
|
96
|
+
expect(picker).not.toBeNull();
|
|
97
|
+
expect(await page.getSelectedVersionLabel(picker!)).toEqual(CLIENT_SDKS_V2_LABEL);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should switch the whole child subtree when the parent version changes', async () => {
|
|
101
|
+
const picker = await page.getSidebarPicker('Client SDKs');
|
|
102
|
+
await page.selectVersionByLabel(picker!, CLIENT_SDKS_V1_LABEL);
|
|
103
|
+
await page.waitForMarkdownTopicLoad();
|
|
104
|
+
|
|
105
|
+
await page.waitForUrlToContain(`${BASE}/v1/android.html`);
|
|
106
|
+
expect(await (await page.getContentHeading()).getText()).toEqual('Android SDK v1');
|
|
107
|
+
|
|
108
|
+
// Sibling child (iOS SDK) should also have moved to the v1 URL, not just this one.
|
|
109
|
+
const iosItem = await page.getSidebarTreeItem('iOS SDK');
|
|
110
|
+
const iosTile = await page.getTreeItemTile(iosItem);
|
|
111
|
+
const iosAnchor = await iosTile.shadow$('a');
|
|
112
|
+
expect(await iosAnchor.getAttribute('href')).toContain('/v1/ios.html');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// TODO: known gap — navigating directly to a different child version URL doesn't sync the
|
|
116
|
+
// parent's picker or sibling hrefs back to that version; both stay stuck on the version the
|
|
117
|
+
// page first loaded with. Enable once that reverse sync ships.
|
|
118
|
+
it.skip('should update the picker and sibling hrefs when navigating directly to a different child version', async () => {
|
|
119
|
+
page = new TopicVersionPage(`${BASE}/v1/android.html`);
|
|
120
|
+
page.open();
|
|
121
|
+
await page.waitForMarkdownTopicLoad();
|
|
122
|
+
|
|
123
|
+
page = new TopicVersionPage(`${BASE}/v2/android.html`);
|
|
124
|
+
page.open();
|
|
125
|
+
await page.waitForMarkdownTopicLoad();
|
|
126
|
+
|
|
127
|
+
const picker = await page.getSidebarPicker('Client SDKs');
|
|
128
|
+
expect(await page.getSelectedVersionLabel(picker!)).toEqual(CLIENT_SDKS_V2_LABEL);
|
|
129
|
+
|
|
130
|
+
const iosItem = await page.getSidebarTreeItem('iOS SDK');
|
|
131
|
+
const iosTile = await page.getTreeItemTile(iosItem);
|
|
132
|
+
const iosAnchor = await iosTile.shadow$('a');
|
|
133
|
+
const href = await iosAnchor.getAttribute('href');
|
|
134
|
+
expect(href).toContain('/ios.html');
|
|
135
|
+
expect(href).not.toContain('/v1/');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('Topic-level versioning > versioned parent with own content, subtopics and a spec subtopic', () => {
|
|
140
|
+
// fixture: Errors and Rate Limits — latest is v2 (ERRORS_V2_LABEL), older is v1 (ERRORS_V1_LABEL).
|
|
141
|
+
beforeEach(async () => {
|
|
142
|
+
page = new TopicVersionPage(`${BASE}/errors-and-rate-limits.html`);
|
|
143
|
+
page.open();
|
|
144
|
+
await page.waitForMarkdownTopicLoad();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should render the picker and default to the latest version', async () => {
|
|
148
|
+
const picker = await page.getSidebarPicker('Errors and Rate Limits');
|
|
149
|
+
expect(picker).not.toBeNull();
|
|
150
|
+
expect(await page.getSelectedVersionLabel(picker!)).toEqual(ERRORS_V2_LABEL);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should switch the parent's own page content when the version changes", async () => {
|
|
154
|
+
const picker = await page.getSidebarPicker('Errors and Rate Limits');
|
|
155
|
+
await page.selectVersionByLabel(picker!, ERRORS_V1_LABEL);
|
|
156
|
+
await page.waitForMarkdownTopicLoad();
|
|
157
|
+
|
|
158
|
+
expect(await (await page.getContentHeading()).getText()).toEqual(
|
|
159
|
+
'Errors and Rate Limits (v1)',
|
|
160
|
+
);
|
|
161
|
+
await page.waitForUrlToContain(`${BASE}/v1/errors-and-rate-limits.html`);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should switch the markdown subtopics when the version changes', async () => {
|
|
165
|
+
const picker = await page.getSidebarPicker('Errors and Rate Limits');
|
|
166
|
+
await page.selectVersionByLabel(picker!, ERRORS_V1_LABEL);
|
|
167
|
+
await page.waitForMarkdownTopicLoad();
|
|
168
|
+
|
|
169
|
+
const child = await page.getSidebarTreeItem('Common Error Codes');
|
|
170
|
+
await child.click();
|
|
171
|
+
await page.waitForMarkdownTopicLoad();
|
|
172
|
+
|
|
173
|
+
expect(await (await page.getContentHeading()).getText()).toEqual('Common Error Codes (v1)');
|
|
174
|
+
await page.waitForUrlToContain(`${BASE}/v1/extra-child-a.html`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should switch the spec (Redoc) subtopic to the matching version when the version changes', async () => {
|
|
178
|
+
// Key cross-type cascade: parent v1 ⇒ the nested spec subtopic is v1 too.
|
|
179
|
+
const picker = await page.getSidebarPicker('Errors and Rate Limits');
|
|
180
|
+
await page.selectVersionByLabel(picker!, ERRORS_V1_LABEL);
|
|
181
|
+
await page.waitForMarkdownTopicLoad();
|
|
182
|
+
|
|
183
|
+
const child = await page.getSidebarTreeItem('Another Spec API');
|
|
184
|
+
await child.click();
|
|
185
|
+
await page.waitForRedocTopicLoad();
|
|
186
|
+
|
|
187
|
+
await page.waitForUrlToContain(`${BASE}/v1/new-sample-api`);
|
|
188
|
+
// v1's spec lacks the `GET /errors/{code}` operation that only v2 has — its absence
|
|
189
|
+
// confirms the v1 spec asset loaded, since both versions share the same spec title.
|
|
190
|
+
const getErrorCodeOperation = await $('a[href*="operation/getErrorCode"]');
|
|
191
|
+
expect(await getErrorCodeOperation.isExisting()).toBeFalsy();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should show the parent topic version selected on the nested Redoc page after Back', async () => {
|
|
195
|
+
const child = await page.getSidebarTreeItem('Another Spec API');
|
|
196
|
+
await child.click();
|
|
197
|
+
await page.waitForRedocTopicLoad();
|
|
198
|
+
|
|
199
|
+
const redocPicker = await page.getRedocPicker();
|
|
200
|
+
await page.selectVersionByLabel(redocPicker!, ERRORS_V1_LABEL);
|
|
201
|
+
await page.waitForRedocTopicLoad();
|
|
202
|
+
|
|
203
|
+
await (await page.getRedocBackLink()).click();
|
|
204
|
+
await page.waitForMarkdownTopicLoad();
|
|
205
|
+
|
|
206
|
+
await page.waitForUrlToContain(`${BASE}/v1/errors-and-rate-limits.html`);
|
|
207
|
+
const parentPicker = await page.getSidebarPicker('Errors and Rate Limits');
|
|
208
|
+
expect(await page.getSelectedVersionLabel(parentPicker!)).toEqual(ERRORS_V1_LABEL);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should return to the latest parent topic when Redoc is switched back to latest', async () => {
|
|
212
|
+
const picker = await page.getSidebarPicker('Errors and Rate Limits');
|
|
213
|
+
await page.selectVersionByLabel(picker!, ERRORS_V1_LABEL);
|
|
214
|
+
await page.waitForMarkdownTopicLoad();
|
|
215
|
+
|
|
216
|
+
const child = await page.getSidebarTreeItem('Another Spec API');
|
|
217
|
+
await child.click();
|
|
218
|
+
await page.waitForRedocTopicLoad();
|
|
219
|
+
|
|
220
|
+
const redocPicker = await page.getRedocPicker();
|
|
221
|
+
await page.selectVersionByLabel(redocPicker!, ERRORS_V2_LABEL);
|
|
222
|
+
await page.waitForRedocTopicLoad();
|
|
223
|
+
|
|
224
|
+
await (await page.getRedocBackLink()).click();
|
|
225
|
+
await page.waitForMarkdownTopicLoad();
|
|
226
|
+
|
|
227
|
+
await page.waitForUrlToEndWith(`${BASE}/errors-and-rate-limits.html`);
|
|
228
|
+
const parentPicker = await page.getSidebarPicker('Errors and Rate Limits');
|
|
229
|
+
expect(await page.getSelectedVersionLabel(parentPicker!)).toEqual(ERRORS_V2_LABEL);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should return to the sibling subtopic on the version selected in Redoc', async () => {
|
|
233
|
+
// Open the spec from this sibling so Back returns here, not to the parent.
|
|
234
|
+
const markdownChild = await page.getSidebarTreeItem('Common Error Codes');
|
|
235
|
+
await markdownChild.click();
|
|
236
|
+
await page.waitForMarkdownTopicLoad();
|
|
237
|
+
|
|
238
|
+
const redocChild = await page.getSidebarTreeItem('Another Spec API');
|
|
239
|
+
await redocChild.click();
|
|
240
|
+
await page.waitForRedocTopicLoad();
|
|
241
|
+
|
|
242
|
+
const redocPicker = await page.getRedocPicker();
|
|
243
|
+
await page.selectVersionByLabel(redocPicker!, ERRORS_V1_LABEL);
|
|
244
|
+
await page.waitForRedocTopicLoad();
|
|
245
|
+
|
|
246
|
+
await (await page.getRedocBackLink()).click();
|
|
247
|
+
await page.waitForMarkdownTopicLoad();
|
|
248
|
+
|
|
249
|
+
await page.waitForUrlToContain(`${BASE}/v1/extra-child-a.html`);
|
|
250
|
+
const siblingChild = await page.getSidebarTreeItem('Common Error Codes');
|
|
251
|
+
const siblingTile = await page.getTreeItemTile(siblingChild);
|
|
252
|
+
const siblingAnchor = await siblingTile.shadow$('a');
|
|
253
|
+
expect(await siblingAnchor.getAttribute('href')).toContain('/v1/extra-child-a.html');
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should return to an unrelated topic unaffected by a version switch on the nested Redoc page', async () => {
|
|
257
|
+
page = new TopicVersionPage(`${BASE}/get-started.html`);
|
|
258
|
+
page.open();
|
|
259
|
+
await page.waitForMarkdownTopicLoad();
|
|
260
|
+
|
|
261
|
+
const child = await page.getSidebarTreeItem('Another Spec API');
|
|
262
|
+
await child.click();
|
|
263
|
+
await page.waitForRedocTopicLoad();
|
|
264
|
+
|
|
265
|
+
const redocPicker = await page.getRedocPicker();
|
|
266
|
+
await page.selectVersionByLabel(redocPicker!, ERRORS_V1_LABEL);
|
|
267
|
+
await page.waitForRedocTopicLoad();
|
|
268
|
+
|
|
269
|
+
await (await page.getRedocBackLink()).click();
|
|
270
|
+
await page.waitForMarkdownTopicLoad();
|
|
271
|
+
|
|
272
|
+
await page.waitForUrlToContain(`${BASE}/get-started.html`);
|
|
273
|
+
expect(await browser.getUrl()).not.toContain('/v1/');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should fall back to the project landing page when the Redoc page is opened with no referrer', async () => {
|
|
277
|
+
// Simulates a fresh direct visit: no stored back target, and `page.open()` (a WebDriver
|
|
278
|
+
// navigation, not a link click) never sets document.referrer either. Routes through
|
|
279
|
+
// globalThis since the tsconfig's lib list omits "DOM" (see readSessionVersionSelections).
|
|
280
|
+
await browser.execute(() =>
|
|
281
|
+
(globalThis as any).sessionStorage.removeItem('redoc-back-target'),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
page = new TopicVersionPage(`${BASE}/new-sample-api`);
|
|
285
|
+
page.open();
|
|
286
|
+
await page.waitForRedocTopicLoad();
|
|
287
|
+
|
|
288
|
+
const redocPicker = await page.getRedocPicker();
|
|
289
|
+
await page.selectVersionByLabel(redocPicker!, ERRORS_V1_LABEL);
|
|
290
|
+
await page.waitForRedocTopicLoad();
|
|
291
|
+
|
|
292
|
+
await (await page.getRedocBackLink()).click();
|
|
293
|
+
await page.waitForMarkdownTopicLoad();
|
|
294
|
+
|
|
295
|
+
// Get Started is the first topic in the TOC, so it's the project's landing page
|
|
296
|
+
// (headerHref) — and it's unversioned, so no version segment should leak onto it either.
|
|
297
|
+
await page.waitForUrlToContain(`${BASE}/get-started.html`);
|
|
298
|
+
expect(await browser.getUrl()).not.toContain('/v1/');
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
describe('Topic-level versioning > standalone Redoc reference', () => {
|
|
303
|
+
// fixture: Widgets API Reference — a spec (forward-arrow) topic, so it never gets a sidebar
|
|
304
|
+
// picker; the picker lives on the Redoc page's own project header instead.
|
|
305
|
+
beforeEach(async () => {
|
|
306
|
+
page = new TopicVersionPage(`${BASE}/widgets-api-reference`);
|
|
307
|
+
page.open();
|
|
308
|
+
await page.waitForRedocTopicLoad();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should render the Redoc version picker and default to the latest version', async () => {
|
|
312
|
+
const picker = await page.getRedocPicker();
|
|
313
|
+
expect(picker).not.toBeNull();
|
|
314
|
+
expect(await page.getSelectedVersionLabel(picker!)).toEqual(WIDGETS_API_REFERENCE_V2_LABEL);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should switch the rendered spec when a different version is selected', async () => {
|
|
318
|
+
const picker = await page.getRedocPicker();
|
|
319
|
+
await page.selectVersionByLabel(picker!, WIDGETS_API_REFERENCE_V1_LABEL);
|
|
320
|
+
await page.waitForRedocTopicLoad();
|
|
321
|
+
|
|
322
|
+
await page.waitForUrlToContain(`${BASE}/v1/widgets-api-reference`);
|
|
323
|
+
const apiInfo = await $('>>>div.api-info');
|
|
324
|
+
expect(await apiInfo.getText()).toContain('1.0.0');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should show the "Newer Version Available" banner when the version is not latest', async () => {
|
|
328
|
+
page = new TopicVersionPage(`${BASE}/v1/widgets-api-reference`);
|
|
329
|
+
page.open();
|
|
330
|
+
await page.waitForRedocTopicLoad();
|
|
331
|
+
|
|
332
|
+
const banner = await page.getRedocVersionBanner();
|
|
333
|
+
expect(banner).not.toBeNull();
|
|
334
|
+
expect(await page.getBannerTitle(banner!)).toEqual('Newer Version Available');
|
|
335
|
+
expect(await page.getBannerLinkHref(banner!)).toContain(`${BASE}/widgets-api-reference`);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should not show the "Newer Version Available" banner when the version is latest', async () => {
|
|
339
|
+
const banner = await page.getRedocVersionBanner();
|
|
340
|
+
expect(banner).toBeNull();
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
describe('Topic-level versioning > URL resolution (markdown)', () => {
|
|
345
|
+
it('should serve the latest version at the canonical URL', async () => {
|
|
346
|
+
page = new TopicVersionPage(`${BASE}/oauth.html`);
|
|
347
|
+
page.open();
|
|
348
|
+
await page.waitForMarkdownTopicLoad();
|
|
349
|
+
expect(await (await page.getContentHeading()).getText()).toEqual('OAuth 2.0');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should serve the latest version at its versioned alias URL', async () => {
|
|
353
|
+
page = new TopicVersionPage(`${BASE}/v2/oauth.html`);
|
|
354
|
+
page.open();
|
|
355
|
+
await page.waitForMarkdownTopicLoad();
|
|
356
|
+
expect(await (await page.getContentHeading()).getText()).toEqual('OAuth 2.0');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should serve an older version at its versioned URL', async () => {
|
|
360
|
+
page = new TopicVersionPage(`${BASE}/v1/oauth.html`);
|
|
361
|
+
page.open();
|
|
362
|
+
await page.waitForMarkdownTopicLoad();
|
|
363
|
+
expect(await (await page.getContentHeading()).getText()).toEqual('OAuth 1.0');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should initialize the picker from the URL version', async () => {
|
|
367
|
+
page = new TopicVersionPage(`${BASE}/v1/oauth.html`);
|
|
368
|
+
page.open();
|
|
369
|
+
await page.waitForMarkdownTopicLoad();
|
|
370
|
+
|
|
371
|
+
const picker = await page.getSidebarPicker('OAuth');
|
|
372
|
+
expect(await page.getSelectedVersionLabel(picker!)).toEqual(OAUTH_V1_LABEL);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('Topic-level versioning > URL resolution (spec)', () => {
|
|
377
|
+
it('should serve the latest spec at the canonical URL', async () => {
|
|
378
|
+
page = new TopicVersionPage(`${BASE}/widgets-api-reference`);
|
|
379
|
+
page.open();
|
|
380
|
+
await page.waitForRedocTopicLoad();
|
|
381
|
+
expect(await (await $('>>>div.api-info')).getText()).toContain('2.0.0');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should serve the latest spec at its versioned alias URL', async () => {
|
|
385
|
+
page = new TopicVersionPage(`${BASE}/v2/widgets-api-reference`);
|
|
386
|
+
page.open();
|
|
387
|
+
await page.waitForRedocTopicLoad();
|
|
388
|
+
expect(await (await $('>>>div.api-info')).getText()).toContain('2.0.0');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('should serve an older spec at its versioned URL', async () => {
|
|
392
|
+
page = new TopicVersionPage(`${BASE}/v1/widgets-api-reference`);
|
|
393
|
+
page.open();
|
|
394
|
+
await page.waitForRedocTopicLoad();
|
|
395
|
+
expect(await (await $('>>>div.api-info')).getText()).toContain('1.0.0');
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('Topic-level versioning > single-version picker (read-only)', () => {
|
|
400
|
+
// A topic with exactly one authored version still renders a picker, but read-only (no
|
|
401
|
+
// dropdown) and never with the "Newer Version Available" banner, since a single version is
|
|
402
|
+
// always the latest by definition.
|
|
403
|
+
// fixture: Widgets API (Beta) — single version 'Beta'.
|
|
404
|
+
it('should render a read-only picker with no dropdown when a Redoc spec has a single version', async () => {
|
|
405
|
+
page = new TopicVersionPage(`${BASE}/widgets-api-beta`);
|
|
406
|
+
page.open();
|
|
407
|
+
await page.waitForRedocTopicLoad();
|
|
408
|
+
const picker = await page.getRedocPicker();
|
|
409
|
+
expect(await page.isReadOnly(picker!)).toBeTruthy();
|
|
410
|
+
expect(await page.getRedocVersionBanner()).toBeNull();
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe('Topic-level versioning > topics without a version picker', () => {
|
|
415
|
+
it('should not render a sidebar picker when the markdown topic is evergreen', async () => {
|
|
416
|
+
page = new TopicVersionPage(`${BASE}/get-started.html`);
|
|
417
|
+
page.open();
|
|
418
|
+
await page.waitForMarkdownTopicLoad();
|
|
419
|
+
|
|
420
|
+
const picker = await page.getSidebarPicker('Get Started');
|
|
421
|
+
expect(picker).toBeNull();
|
|
422
|
+
expect(await browser.getUrl()).not.toMatch(/\/v\d+\//);
|
|
423
|
+
expect(await page.getMarkdownVersionBanner()).toBeNull();
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// fixture: Widgets Status API — plain spec:, no versions: at all.
|
|
427
|
+
it('should not render a picker when the Redoc spec is evergreen', async () => {
|
|
428
|
+
page = new TopicVersionPage(`${BASE}/widgets-status-api`);
|
|
429
|
+
page.open();
|
|
430
|
+
await page.waitForRedocTopicLoad();
|
|
431
|
+
expect(await page.getRedocPicker()).toBeNull();
|
|
432
|
+
expect(await page.getRedocVersionBanner()).toBeNull();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should show the picker only on versioned rows when the sidebar is mixed', async () => {
|
|
436
|
+
page = new TopicVersionPage(`${BASE}/get-started.html`);
|
|
437
|
+
page.open();
|
|
438
|
+
await page.waitForMarkdownTopicLoad();
|
|
439
|
+
await page.waitForSidebar();
|
|
440
|
+
|
|
441
|
+
expect(await page.getSidebarPicker('OAuth')).not.toBeNull();
|
|
442
|
+
expect(await page.getSidebarPicker('Errors and Rate Limits')).not.toBeNull();
|
|
443
|
+
expect(await page.getSidebarPicker('Client SDKs')).not.toBeNull();
|
|
444
|
+
expect(await page.getSidebarPicker('Get Started')).toBeNull();
|
|
445
|
+
expect(await page.getSidebarPicker('Widget Concepts')).toBeNull();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should not leak a version segment when navigating to an evergreen topic', async () => {
|
|
449
|
+
page = new TopicVersionPage(`${BASE}/v1/oauth.html`);
|
|
450
|
+
page.open();
|
|
451
|
+
await page.waitForMarkdownTopicLoad();
|
|
452
|
+
|
|
453
|
+
const getStarted = await page.getSidebarTreeItem('Get Started');
|
|
454
|
+
await getStarted.click();
|
|
455
|
+
await page.waitForMarkdownTopicLoad();
|
|
456
|
+
|
|
457
|
+
await page.waitForUrlToContain(`${BASE}/get-started.html`);
|
|
458
|
+
expect(await browser.getUrl()).not.toContain('/v1/');
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('Topic-level versioning > persistence on reload', () => {
|
|
463
|
+
it('should keep the selected version and its content after a refresh', async () => {
|
|
464
|
+
page = new TopicVersionPage(`${BASE}/oauth.html`);
|
|
465
|
+
page.open();
|
|
466
|
+
await page.waitForMarkdownTopicLoad();
|
|
467
|
+
|
|
468
|
+
const picker = await page.getSidebarPicker('OAuth');
|
|
469
|
+
await page.selectVersionByLabel(picker!, OAUTH_V1_LABEL);
|
|
470
|
+
await page.waitForMarkdownTopicLoad();
|
|
471
|
+
|
|
472
|
+
await browser.refresh();
|
|
473
|
+
await page.waitForMarkdownTopicLoad();
|
|
474
|
+
|
|
475
|
+
const pickerAfterReload = await page.getSidebarPicker('OAuth');
|
|
476
|
+
expect(await page.getSelectedVersionLabel(pickerAfterReload!)).toEqual(OAUTH_V1_LABEL);
|
|
477
|
+
expect(await (await page.getContentHeading()).getText()).toEqual('OAuth 1.0');
|
|
478
|
+
expect(await browser.getUrl()).toContain(`${BASE}/v1/oauth.html`);
|
|
479
|
+
|
|
480
|
+
const selections = await page.readSessionVersionSelections();
|
|
481
|
+
expect(selections).not.toBeNull();
|
|
482
|
+
expect(selections![`${BASE}/oauth.html`]).toEqual('v1');
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
describe('Topic-level versioning > invalid version in URL', () => {
|
|
487
|
+
it('should return a 404 when the markdown URL has an invalid version', async () => {
|
|
488
|
+
browser.url(`${BASE}/v999/oauth.html`);
|
|
489
|
+
|
|
490
|
+
const errorComponent = await $('dx-error');
|
|
491
|
+
await errorComponent.waitForDisplayed();
|
|
492
|
+
expect(await browser.getTitle()).toEqual('404 Error | Salesforce Developers');
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should return a 404 when the spec URL has an invalid version', async () => {
|
|
496
|
+
browser.url(`${BASE}/v999/widgets-api-reference`);
|
|
497
|
+
|
|
498
|
+
const errorComponent = await $('dx-error');
|
|
499
|
+
await errorComponent.waitForDisplayed();
|
|
500
|
+
expect(await browser.getTitle()).toEqual('404 Error | Salesforce Developers');
|
|
501
|
+
});
|
|
502
|
+
});
|