@salesforcedevs/sfdocs-e2e 1.0.12-fix-redoc-topic-v-1 → 1.0.12-fix-redoc-topic-v-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.
@@ -89,14 +89,10 @@ 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
92
+ chrome,
93
+ firefox: { version: '0.35.0' }, // https://github.com/mozilla/geckodriver/releases - 0.30.0+ supports ARM64 Mac
93
94
  };
94
95
 
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
-
100
96
  exports.config = {
101
97
  //
102
98
  // ====================
@@ -192,8 +188,8 @@ exports.config = {
192
188
  [
193
189
  'selenium-standalone',
194
190
  {
195
- installArgs: { version: seleniumServerVersion, drivers }, // drivers to install
196
- args: { version: seleniumServerVersion, drivers }, // drivers to use
191
+ installArgs: { drivers }, // drivers to install
192
+ args: { drivers }, // drivers to use
197
193
  },
198
194
  ],
199
195
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/sfdocs-e2e",
3
- "version": "1.0.12-fix-redoc-topic-v-1",
3
+ "version": "1.0.12-fix-redoc-topic-v-2",
4
4
  "description": "E2E test suite for SFDocs static portal pages (SSG)",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -0,0 +1,101 @@
1
+ class ContentActionToolbar {
2
+ HOST_SELECTOR = 'doc-content-action-toolbar';
3
+ INLINE_BUTTON_SELECTOR = 'dx-button.toolbar-button';
4
+ MENU_TRIGGER_SELECTOR = 'dx-button.menu-trigger';
5
+ MENU_SELECTOR = 'dx-dropdown.toolbar-menu';
6
+ MENU_GROUP_SELECTOR = 'div.menu_option';
7
+ MENU_HEADING_SELECTOR = 'span.menu_heading';
8
+ MENU_OPTION_SELECTOR = 'dx-dropdown-option';
9
+ OPTION_LABEL_SELECTOR = 'div.option_label';
10
+ COPIED_TOOLTIP_SELECTOR = '[role="tooltip"].is-visible';
11
+
12
+ async get(): Promise<WebdriverIO.Element> {
13
+ const toolbar = await $(`>>>${this.HOST_SELECTOR}`);
14
+ await toolbar.waitForDisplayed();
15
+ return toolbar;
16
+ }
17
+
18
+ async isPresent(): Promise<boolean> {
19
+ return (await $(`>>>${this.HOST_SELECTOR}`)).isExisting();
20
+ }
21
+
22
+ async getInlineButtonLabels(toolbar: WebdriverIO.Element): Promise<string[]> {
23
+ const inlineButtons = await toolbar.shadow$$(this.INLINE_BUTTON_SELECTOR);
24
+ return Promise.all(inlineButtons.map(async (button) => (await button.getText()).trim()));
25
+ }
26
+
27
+ async openMoreActionsMenu(toolbar: WebdriverIO.Element): Promise<void> {
28
+ const trigger = await toolbar.shadow$(this.MENU_TRIGGER_SELECTOR);
29
+ await trigger.waitForClickable();
30
+ await trigger.click();
31
+ }
32
+
33
+ async getMenuOptionLabelsByGroup(
34
+ toolbar: WebdriverIO.Element,
35
+ ): Promise<Record<string, string[]>> {
36
+ const dropdown = await toolbar.shadow$(this.MENU_SELECTOR);
37
+ const groupContainers = await dropdown.shadow$$(this.MENU_GROUP_SELECTOR);
38
+
39
+ const labelsByGroup: Record<string, string[]> = {};
40
+ for (const group of groupContainers) {
41
+ const heading = (await (await group.$(this.MENU_HEADING_SELECTOR)).getText()).trim();
42
+ const options = await group.$$(this.MENU_OPTION_SELECTOR);
43
+ labelsByGroup[heading] = await Promise.all(
44
+ options.map(async (option) =>
45
+ (await (await option.shadow$(this.OPTION_LABEL_SELECTOR)).getText()).trim(),
46
+ ),
47
+ );
48
+ }
49
+ return labelsByGroup;
50
+ }
51
+
52
+ async clickMenuOption(toolbar: WebdriverIO.Element, label: string): Promise<void> {
53
+ const dropdown = await toolbar.shadow$(this.MENU_SELECTOR);
54
+ const options = await dropdown.shadow$$(this.MENU_OPTION_SELECTOR);
55
+ for (const option of options) {
56
+ const optionLabel = (
57
+ await (await option.shadow$(this.OPTION_LABEL_SELECTOR)).getText()
58
+ ).trim();
59
+ if (optionLabel === label) {
60
+ await option.click();
61
+ return;
62
+ }
63
+ }
64
+ throw new Error(`Menu option "${label}" not found`);
65
+ }
66
+
67
+ async getUrlFromNewTab(action: () => Promise<void>): Promise<string> {
68
+ const originalHandle = await browser.getWindowHandle();
69
+ const handlesBefore = await browser.getWindowHandles();
70
+
71
+ await action();
72
+
73
+ await browser.waitUntil(
74
+ async (): Promise<boolean> =>
75
+ (await browser.getWindowHandles()).length > handlesBefore.length,
76
+ {
77
+ timeout: 10000,
78
+ timeoutMsg: 'Expected a new browser tab to open',
79
+ },
80
+ );
81
+
82
+ const handlesAfter = await browser.getWindowHandles();
83
+ const newHandle = handlesAfter.find((handle) => !handlesBefore.includes(handle));
84
+ if (!newHandle) {
85
+ throw new Error('New browser tab opened but its handle could not be resolved');
86
+ }
87
+ await browser.switchToWindow(newHandle);
88
+ const url = await browser.getUrl();
89
+
90
+ await browser.closeWindow();
91
+ await browser.switchToWindow(originalHandle);
92
+ return url;
93
+ }
94
+
95
+ async getVisibleCopiedTooltipLabel(): Promise<string> {
96
+ const tooltip = await $(`>>>${this.COPIED_TOOLTIP_SELECTOR}`);
97
+ return (await tooltip.getAttribute('aria-label'))?.trim() ?? '';
98
+ }
99
+ }
100
+
101
+ export default new ContentActionToolbar();
@@ -111,6 +111,17 @@ export default class Page {
111
111
  return (await label.getText()).trim();
112
112
  }
113
113
 
114
+ /**
115
+ * Toggles a parent tree item open without navigating — the expand icon's own click handler
116
+ * calls preventDefault/stopPropagation, unlike the tile's main link. Needed to reach a child
117
+ * row (e.g. a nested Redoc spec) that isn't rendered in the DOM until its parent is expanded.
118
+ */
119
+ async expandTreeItem(treeItem: WebdriverIO.Element): Promise<void> {
120
+ const tile = await this.getTreeItemTile(treeItem);
121
+ const icon = await tile.shadow$('.tile-expand-icon dx-icon');
122
+ await icon.click();
123
+ }
124
+
114
125
  async findTreeItemByLabel(
115
126
  treeItem: WebdriverIO.Element,
116
127
  label: string,
@@ -63,6 +63,26 @@ export default class TopicVersionPage extends Page {
63
63
  return wrapper.$('doc-version-picker');
64
64
  }
65
65
 
66
+ /** Clicks a sidebar tree item and waits for the Redoc page it opens to load. */
67
+ async openRedocFromSidebar(label: string): Promise<void> {
68
+ const item = await this.getSidebarTreeItem(label);
69
+ await item.click();
70
+ await this.waitForRedocTopicLoad();
71
+ }
72
+
73
+ /** Selects a version on the Redoc page's own picker and waits for the new spec to load. */
74
+ async selectRedocVersionByLabel(label: string): Promise<void> {
75
+ const picker = await this.getRedocPicker();
76
+ await this.selectVersionByLabel(picker!, label);
77
+ await this.waitForRedocTopicLoad();
78
+ }
79
+
80
+ /** Clicks the Redoc back link and waits for the markdown page it returns to load. */
81
+ async goBackFromRedoc(): Promise<void> {
82
+ await (await this.getRedocBackLink()).click();
83
+ await this.waitForMarkdownTopicLoad();
84
+ }
85
+
66
86
  /**
67
87
  * The dismissible "Newer Version Available" banner shown on a non-latest Redoc spec version.
68
88
  * Reuses the `doc-phase` component, inserted at the top of `.api-content`. Returns null when
@@ -1,6 +1,7 @@
1
1
  import DocsPage from '../pageobjects/docs.page';
2
2
  import GuidePage from '../pageobjects/guide.page';
3
3
  import { ReferencePage } from '../pageobjects/reference.page';
4
+ import contentActionToolbar from '../pageobjects/content-action-toolbar';
4
5
 
5
6
  const DOCS_MARKDOWN_PATH = '/docs/docs-content-type-example/get-started.html';
6
7
 
@@ -10,63 +11,154 @@ const EN_US_MARKDOWN_REFERENCE_PATH =
10
11
  const NON_MARKDOWN_REFERENCE_PATH = '/docs/test-metamind-oas-api/references/1mkfx9?meta=Summary';
11
12
  const NON_EN_US_MARKDOWN_REFERENCE_PATH =
12
13
  '/docs/commerce/ja-jp/flex-references-loc/references/manual-ref/commerce-cloud-api-overview.html';
13
-
14
- async function expectToolbarButtons(toolbar: WebdriverIO.Element): Promise<void> {
15
- const toolbarButtons = await toolbar.shadow$$('dx-button.toolbar-button');
16
- expect(toolbarButtons.length).toBe(3);
17
-
18
- const labels = await Promise.all(
19
- toolbarButtons.map(async (button) => (await button.getText()).trim()),
20
- );
21
- expect(labels).toEqual(
22
- expect.arrayContaining(['Copy as Markdown', 'View as Markdown', 'Copy URL to Markdown']),
23
- );
24
- }
14
+ const INLINE_MARKDOWN_LABELS = ['Copy as Markdown', 'View as Markdown', 'View llms.txt'];
15
+ const MENU_GROUPS = {
16
+ 'This Page': [
17
+ 'Copy as Markdown',
18
+ 'View as Markdown',
19
+ 'Copy URL to Markdown',
20
+ 'Open in ChatGPT',
21
+ 'Open in Claude',
22
+ 'Open in Perplexity',
23
+ ],
24
+ 'All Documentation': ['View llms.txt', 'Copy URL to llms.txt'],
25
+ };
26
+ const AI_ASSISTANTS = [
27
+ { label: 'Open in ChatGPT', host: 'chatgpt.com' },
28
+ { label: 'Open in Claude', host: 'claude.ai' },
29
+ { label: 'Open in Perplexity', host: 'www.perplexity.ai' },
30
+ ];
31
+ const LLMS_TXT_PATH = '/docs/llms.txt';
32
+ const COPIED_TOOLTIP_LABEL = 'Copied!';
25
33
 
26
34
  describe('Content action toolbar scenarios', () => {
27
- it('Markdown guide should have the toolbar', async () => {
35
+ it('should show the inline markdown actions when opening a markdown guide', async () => {
28
36
  const page = new GuidePage(EN_US_GUIDE_PATH);
29
37
  page.open();
30
38
  await page.waitPageLoad();
31
39
 
32
- const toolbar = await $('>>>doc-content-action-toolbar');
33
- await toolbar.waitForDisplayed();
34
- await expectToolbarButtons(toolbar);
40
+ const toolbar = await contentActionToolbar.get();
41
+ expect(await contentActionToolbar.getInlineButtonLabels(toolbar)).toEqual(
42
+ expect.arrayContaining(INLINE_MARKDOWN_LABELS),
43
+ );
35
44
  });
36
45
 
37
- it('Markdown reference should have the toolbar', async () => {
46
+ it('should show the inline markdown actions when opening a markdown reference', async () => {
38
47
  const referencePage = new ReferencePage(EN_US_MARKDOWN_REFERENCE_PATH);
39
48
  referencePage.open();
40
49
  await referencePage.waitForMarkdownBasedReferencePageLoad();
41
50
 
42
- const toolbar = await $('>>>doc-content-action-toolbar');
43
- await toolbar.waitForDisplayed();
44
- await expectToolbarButtons(toolbar);
51
+ const toolbar = await contentActionToolbar.get();
52
+ expect(await contentActionToolbar.getInlineButtonLabels(toolbar)).toEqual(
53
+ expect.arrayContaining(INLINE_MARKDOWN_LABELS),
54
+ );
45
55
  });
46
56
 
47
- it('Markdown docs page should have the toolbar', async () => {
57
+ it('should show the inline markdown actions when opening a markdown docs page', async () => {
48
58
  const page = new DocsPage(DOCS_MARKDOWN_PATH);
49
59
  page.open();
50
60
  await page.waitPageLoad();
51
61
 
52
- const toolbar = await $('>>>doc-content-action-toolbar');
53
- await toolbar.waitForDisplayed();
54
- await expectToolbarButtons(toolbar);
62
+ const toolbar = await contentActionToolbar.get();
63
+ expect(await contentActionToolbar.getInlineButtonLabels(toolbar)).toEqual(
64
+ expect.arrayContaining(INLINE_MARKDOWN_LABELS),
65
+ );
66
+ });
67
+
68
+ it('should expose the full grouped action set through the More actions menu on a markdown page', async () => {
69
+ const page = new GuidePage(EN_US_GUIDE_PATH);
70
+ page.open();
71
+ await page.waitPageLoad();
72
+
73
+ const toolbar = await contentActionToolbar.get();
74
+ await contentActionToolbar.openMoreActionsMenu(toolbar);
75
+
76
+ const labelsByGroup = await contentActionToolbar.getMenuOptionLabelsByGroup(toolbar);
77
+ expect(Object.keys(labelsByGroup)).toEqual(
78
+ expect.arrayContaining(Object.keys(MENU_GROUPS)),
79
+ );
80
+ for (const [group, expectedLabels] of Object.entries(MENU_GROUPS)) {
81
+ expect(labelsByGroup[group]).toEqual(expect.arrayContaining(expectedLabels));
82
+ }
55
83
  });
56
84
 
57
- it("Non-markdown reference shouldn't have toolbar", async () => {
85
+ AI_ASSISTANTS.forEach(({ label, host }) => {
86
+ it(`should open ${host} with the page context when selecting ${label}`, async () => {
87
+ const page = new GuidePage(EN_US_GUIDE_PATH);
88
+ page.open();
89
+ await page.waitPageLoad();
90
+
91
+ const toolbar = await contentActionToolbar.get();
92
+ await contentActionToolbar.openMoreActionsMenu(toolbar);
93
+
94
+ const url = await contentActionToolbar.getUrlFromNewTab(() =>
95
+ contentActionToolbar.clickMenuOption(toolbar, label),
96
+ );
97
+ expect(url).toContain(host);
98
+ });
99
+ });
100
+
101
+ it('should open the markdown version of the page in a new tab when selecting View as Markdown', async () => {
102
+ const page = new GuidePage(EN_US_GUIDE_PATH);
103
+ page.open();
104
+ await page.waitPageLoad();
105
+
106
+ const toolbar = await contentActionToolbar.get();
107
+ await contentActionToolbar.openMoreActionsMenu(toolbar);
108
+
109
+ const url = await contentActionToolbar.getUrlFromNewTab(() =>
110
+ contentActionToolbar.clickMenuOption(toolbar, 'View as Markdown'),
111
+ );
112
+ expect(url.endsWith('.md')).toBe(true);
113
+ });
114
+
115
+ it('should open the site llms.txt in a new tab when selecting View llms.txt', async () => {
116
+ const page = new GuidePage(EN_US_GUIDE_PATH);
117
+ page.open();
118
+ await page.waitPageLoad();
119
+
120
+ const toolbar = await contentActionToolbar.get();
121
+ await contentActionToolbar.openMoreActionsMenu(toolbar);
122
+
123
+ const url = await contentActionToolbar.getUrlFromNewTab(() =>
124
+ contentActionToolbar.clickMenuOption(toolbar, 'View llms.txt'),
125
+ );
126
+ expect(url.endsWith(LLMS_TXT_PATH)).toBe(true);
127
+ });
128
+
129
+ it('should flash the Copied! tooltip when selecting Copy URL to Markdown', async () => {
130
+ const page = new GuidePage(EN_US_GUIDE_PATH);
131
+ page.open();
132
+ await page.waitPageLoad();
133
+
134
+ const toolbar = await contentActionToolbar.get();
135
+ await contentActionToolbar.openMoreActionsMenu(toolbar);
136
+ await contentActionToolbar.clickMenuOption(toolbar, 'Copy URL to Markdown');
137
+
138
+ await browser.waitUntil(
139
+ async () =>
140
+ (await contentActionToolbar.getVisibleCopiedTooltipLabel()) ===
141
+ COPIED_TOOLTIP_LABEL,
142
+ {
143
+ timeout: 5000,
144
+ timeoutMsg: 'Expected the menu tooltip to flash "Copied!"',
145
+ },
146
+ );
147
+ });
148
+
149
+ it('should not show the toolbar on a non-markdown reference', async () => {
58
150
  const referencePage = new ReferencePage(NON_MARKDOWN_REFERENCE_PATH);
59
151
  referencePage.open();
60
152
  await referencePage.waitForSpecBasedReferencePageLoad();
61
153
 
62
- const toolbar = await $('>>>doc-content-action-toolbar');
63
- expect(await toolbar.isExisting()).toBeFalsy();
154
+ expect(await contentActionToolbar.isPresent()).toBeFalsy();
64
155
  });
65
156
 
66
- it("Non en-us locale page shouldn't have toolbar", async () => {
157
+ it('should not show the toolbar on a non en-us page', async () => {
67
158
  const nonEnUsReferencePage = new ReferencePage(NON_EN_US_MARKDOWN_REFERENCE_PATH);
68
159
  nonEnUsReferencePage.open();
69
160
  await nonEnUsReferencePage.waitForMarkdownBasedReferencePageLoad();
70
- expect(await (await $('>>>doc-content-action-toolbar')).isExisting()).toBeFalsy();
161
+
162
+ expect(await contentActionToolbar.isPresent()).toBeFalsy();
71
163
  });
72
164
  });
@@ -180,9 +180,7 @@ describe('Topic-level versioning > versioned parent with own content, subtopics
180
180
  await page.selectVersionByLabel(picker!, ERRORS_V1_LABEL);
181
181
  await page.waitForMarkdownTopicLoad();
182
182
 
183
- const child = await page.getSidebarTreeItem('Another Spec API');
184
- await child.click();
185
- await page.waitForRedocTopicLoad();
183
+ await page.openRedocFromSidebar('Another Spec API');
186
184
 
187
185
  await page.waitForUrlToContain(`${BASE}/v1/new-sample-api`);
188
186
  // v1's spec lacks the `GET /errors/{code}` operation that only v2 has — its absence
@@ -192,16 +190,9 @@ describe('Topic-level versioning > versioned parent with own content, subtopics
192
190
  });
193
191
 
194
192
  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();
193
+ await page.openRedocFromSidebar('Another Spec API');
194
+ await page.selectRedocVersionByLabel(ERRORS_V1_LABEL);
195
+ await page.goBackFromRedoc();
205
196
 
206
197
  await page.waitForUrlToContain(`${BASE}/v1/errors-and-rate-limits.html`);
207
198
  const parentPicker = await page.getSidebarPicker('Errors and Rate Limits');
@@ -213,16 +204,9 @@ describe('Topic-level versioning > versioned parent with own content, subtopics
213
204
  await page.selectVersionByLabel(picker!, ERRORS_V1_LABEL);
214
205
  await page.waitForMarkdownTopicLoad();
215
206
 
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();
207
+ await page.openRedocFromSidebar('Another Spec API');
208
+ await page.selectRedocVersionByLabel(ERRORS_V2_LABEL);
209
+ await page.goBackFromRedoc();
226
210
 
227
211
  await page.waitForUrlToEndWith(`${BASE}/errors-and-rate-limits.html`);
228
212
  const parentPicker = await page.getSidebarPicker('Errors and Rate Limits');
@@ -235,16 +219,9 @@ describe('Topic-level versioning > versioned parent with own content, subtopics
235
219
  await markdownChild.click();
236
220
  await page.waitForMarkdownTopicLoad();
237
221
 
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();
222
+ await page.openRedocFromSidebar('Another Spec API');
223
+ await page.selectRedocVersionByLabel(ERRORS_V1_LABEL);
224
+ await page.goBackFromRedoc();
248
225
 
249
226
  await page.waitForUrlToContain(`${BASE}/v1/extra-child-a.html`);
250
227
  const siblingChild = await page.getSidebarTreeItem('Common Error Codes');
@@ -258,16 +235,14 @@ describe('Topic-level versioning > versioned parent with own content, subtopics
258
235
  page.open();
259
236
  await page.waitForMarkdownTopicLoad();
260
237
 
261
- const child = await page.getSidebarTreeItem('Another Spec API');
262
- await child.click();
263
- await page.waitForRedocTopicLoad();
238
+ // "Another Spec API" isn't rendered in the DOM until its parent is expanded — expanding
239
+ // doesn't navigate, so Get Started stays the referrer once we click into the spec below.
240
+ const parent = await page.getSidebarTreeItem('Errors and Rate Limits');
241
+ await page.expandTreeItem(parent);
264
242
 
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();
243
+ await page.openRedocFromSidebar('Another Spec API');
244
+ await page.selectRedocVersionByLabel(ERRORS_V1_LABEL);
245
+ await page.goBackFromRedoc();
271
246
 
272
247
  await page.waitForUrlToContain(`${BASE}/get-started.html`);
273
248
  expect(await browser.getUrl()).not.toContain('/v1/');
@@ -285,12 +260,8 @@ describe('Topic-level versioning > versioned parent with own content, subtopics
285
260
  page.open();
286
261
  await page.waitForRedocTopicLoad();
287
262
 
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();
263
+ await page.selectRedocVersionByLabel(ERRORS_V1_LABEL);
264
+ await page.goBackFromRedoc();
294
265
 
295
266
  // Get Started is the first topic in the TOC, so it's the project's landing page
296
267
  // (headerHref) — and it's unversioned, so no version segment should leak onto it either.