@salesforcedevs/sfdocs-e2e 1.0.4-skip-footer → 1.0.5-alpha

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/README.md CHANGED
@@ -43,6 +43,11 @@ Tests are organised into three suites, each targeting a different surface:
43
43
  - Google Chrome installed (ChromeDriver version is auto-detected from your local Chrome installation)
44
44
  - A running SFDocs portal at the target base URL
45
45
 
46
+ The `static-portal` suite excludes the following tests that are not applicable to static builds:
47
+
48
+ - `docs.api.test.ts` and `docs.test.ts` — landing pages that don't exist in static builds
49
+ - Footer tests (`should display footer`) — footer component is not available in static builds
50
+
46
51
  ### Run against the static portal (default)
47
52
 
48
53
  ```bash
@@ -101,6 +106,7 @@ npx sfdocs-e2e --baseUrl http://localhost:8080
101
106
  # Choose a config suite
102
107
  npx sfdocs-e2e --config portal
103
108
  npx sfdocs-e2e --config static-portal
109
+ npx sfdocs-e2e --config static-dsc-portal
104
110
  npx sfdocs-e2e --config search
105
111
 
106
112
  # Help
@@ -154,13 +160,18 @@ Publish a new version when:
154
160
 
155
161
  Always validate with an alpha release first, then promote to stable.
156
162
 
157
- **1. Publish an alpha (for testing)**
163
+ The e2e package is marked `"private": true` so it is automatically skipped during the monorepo's standard `yarn release` workflow. Two convenience scripts handle temporarily removing the private flag, publishing, and restoring it:
164
+
165
+ | Script | Command | Description |
166
+ | ------------------- | ---------------------------------- | ---------------------------------- |
167
+ | `release:alpha:e2e` | `yarn release:alpha:e2e <version>` | Publish with the `alpha` dist-tag |
168
+ | `release:e2e` | `yarn release:e2e <version>` | Publish with the `latest` dist-tag |
158
169
 
159
- > The e2e package is marked `"private": true` so it is automatically skipped during the monorepo's standard `yarn release` workflow.
170
+ **1. Publish an alpha (for testing)**
160
171
 
161
172
  ```bash
162
173
  cd packages/e2e
163
- yarn publish --tag alpha --new-version 1.0.0-alpha.0
174
+ yarn release:alpha:e2e 1.0.0-alpha.0
164
175
  ```
165
176
 
166
177
  The `alpha` dist-tag means `yarn add @salesforcedevs/sfdocs-e2e` will **not** resolve this version by default — consumers must opt in explicitly:
@@ -179,19 +190,20 @@ Iterate on alphas as needed (`1.0.0-alpha.1`, `1.0.0-alpha.2`, ...) until the re
179
190
 
180
191
  ```bash
181
192
  cd packages/e2e
182
- yarn publish --tag latest --new-version 1.0.0
193
+ yarn release:e2e 1.0.0
183
194
  ```
184
195
 
185
196
  **3. Future releases — bump and publish**
186
197
 
187
198
  ```bash
188
- # Bug fix
189
199
  cd packages/e2e
190
- yarn publish --tag latest --new-version 1.0.1
200
+
201
+ # Bug fix
202
+ yarn release:e2e 1.0.1
191
203
 
192
204
  # New feature
193
- yarn publish --tag latest --new-version 1.1.0
205
+ yarn release:e2e 1.1.0
194
206
 
195
207
  # Breaking change
196
- yarn publish --tag latest --new-version 2.0.0
208
+ yarn release:e2e 2.0.0
197
209
  ```
package/bin/publish.sh ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ VERSION="${1:?Usage: publish.sh <version> [--tag <tag>]}"
5
+ shift
6
+
7
+ cp package.json .package.json.bak
8
+
9
+ cleanup() {
10
+ mv .package.json.bak package.json
11
+ node -e "
12
+ var p = require('./package.json');
13
+ p.version = '$VERSION';
14
+ require('fs').writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n');
15
+ "
16
+ git add package.json
17
+ git commit --amend --no-edit
18
+ }
19
+ trap cleanup EXIT
20
+
21
+ node -e "
22
+ var p = require('./package.json');
23
+ delete p.private;
24
+ require('fs').writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n');
25
+ "
26
+
27
+ if [ "$#" -gt 0 ]; then
28
+ yarn publish --new-version "$VERSION" "$@"
29
+ else
30
+ yarn publish --new-version "$VERSION"
31
+ fi
package/bin/run-e2e.js CHANGED
@@ -56,6 +56,7 @@ Environment Variables:
56
56
  const configName = parsedArgs.config || 'static-portal';
57
57
  const configMap = {
58
58
  'static-portal': 'static-portal-wdio.conf.js',
59
+ 'static-dsc-portal': 'static-dsc-portal-wdio.conf.js',
59
60
  portal: 'portal-wdio.conf.js',
60
61
  search: 'search-wdio.conf.js',
61
62
  };
@@ -80,6 +80,7 @@ const defaultCapabilities = [
80
80
  },
81
81
  ];
82
82
  const defaultMochaTimeoutInterval = 200000; //increased timeout because its taking more time to load for references file
83
+ const isGithubActions = Boolean(process.env.GITHUB_ACTIONS);
83
84
 
84
85
  // Resolve ChromeDriver version: env var → Chrome for Testing API lookup → latest
85
86
  const resolvedChromeDriverVersion = resolveChromedriverVersion();
@@ -181,15 +182,25 @@ exports.config = {
181
182
  // Services take over a specific job you don't want to take care of. They enhance
182
183
  // your test setup with almost no effort. Unlike plugins, they don't add new
183
184
  // commands. Instead, they hook themselves up into the test process.
184
- services: [
185
- [
186
- 'selenium-standalone',
187
- {
188
- installArgs: { drivers }, // drivers to install
189
- args: { drivers }, // drivers to use
190
- },
191
- ],
192
- ],
185
+ services: isGithubActions
186
+ ? []
187
+ : [
188
+ [
189
+ 'selenium-standalone',
190
+ {
191
+ installArgs: { drivers }, // drivers to install
192
+ args: { drivers }, // drivers to use
193
+ },
194
+ ],
195
+ ],
196
+ ...(isGithubActions
197
+ ? {
198
+ protocol: 'http',
199
+ hostname: '127.0.0.1',
200
+ port: 4444,
201
+ path: '/wd/hub',
202
+ }
203
+ : {}),
193
204
 
194
205
  // Framework you want to run your specs with.
195
206
  // The following are supported: Mocha, Jasmine, and Cucumber
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/sfdocs-e2e",
3
- "version": "1.0.4-skip-footer",
3
+ "version": "1.0.5-alpha",
4
4
  "description": "E2E test suite for SFDocs static portal pages (SSG)",
5
5
  "license": "ISC",
6
6
  "bin": {
@@ -16,14 +16,13 @@
16
16
  "test:e2e": "TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=./tsconfig.json wdio wdio.conf.js",
17
17
  "test:e2e:portal": "TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=./tsconfig.json wdio portal-wdio.conf.js",
18
18
  "test:e2e:static:portal": "TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=./tsconfig.json wdio static-portal-wdio.conf.js",
19
+ "test:e2e:static:dsc:portal": "TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=./tsconfig.json wdio static-dsc-portal-wdio.conf.js",
19
20
  "test:scope-search": "TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=./tsconfig.json wdio search-wdio.conf.js",
20
- "prepublish:e2e": "node -e \"const p=require('./package.json'); delete p.private; require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2)+'\\n')\"",
21
- "postpublish:e2e": "node -e \"const p=require('./package.json'); p.private=true; require('fs').writeFileSync('./package.json', JSON.stringify(p, null, 2)+'\\n')\"",
22
- "release:alpha:e2e": "f() { yarn prepublish:e2e && yarn publish --tag alpha --new-version $1; yarn postpublish:e2e; }; f",
23
- "release:e2e": "f() { yarn prepublish:e2e && yarn publish --new-version $1; yarn postpublish:e2e; }; f"
21
+ "release:alpha:e2e": "f() { bash bin/publish.sh $1 --tag alpha; }; f",
22
+ "release:e2e": "f() { bash bin/publish.sh $1; }; f"
24
23
  },
25
24
  "dependencies": {
26
- "@sa11y/wdio": "^7.0.0",
25
+ "@sa11y/wdio": "^8.0.0",
27
26
  "@wdio/allure-reporter": "^7.19.0",
28
27
  "@wdio/cli": "^7.19.0",
29
28
  "@wdio/local-runner": "^7.19.0",
@@ -13,7 +13,7 @@ describe('Homepage', () => {
13
13
  it('should show the expected number of projects', async () => {
14
14
  const projectCards = await HomePage.projectCards;
15
15
 
16
- expect(projectCards.length).toEqual(17);
16
+ expect(projectCards.length).toEqual(20);
17
17
  });
18
18
 
19
19
  // it('is accessible', () => {
@@ -44,15 +44,15 @@ export default class Page {
44
44
  );
45
45
  }
46
46
 
47
- async getHeader(): Promise<WebdriverIO.Element> {
48
- return await $('hgf-c360contextnav');
47
+ async getHeader(headerTag = 'dx-global-header'): Promise<WebdriverIO.Element> {
48
+ return await $(headerTag);
49
49
  }
50
50
 
51
51
  async getHeaderTitle(): Promise<WebdriverIO.Element> {
52
52
  return (await $('>>>header')).$('h4');
53
53
  }
54
54
 
55
- async getGlobalSearchBox(headerTag = 'dx-header'): Promise<WebdriverIO.Element> {
55
+ async getGlobalSearchBox(headerTag = 'dx-global-header'): Promise<WebdriverIO.Element> {
56
56
  return (await $(headerTag)).shadow$('dx-header-search');
57
57
  }
58
58
 
@@ -0,0 +1,70 @@
1
+ import GuidePage from '../pageobjects/guide.page';
2
+
3
+ /**
4
+ * E2E tests for internal link resolution (internalReferencePlugin + referenceLinkTransformerPlugin).
5
+ * Covered: relative .md → .html resolution, guide-to-guide links, reference-to-guide links,
6
+ * external links.
7
+ **/
8
+ describe('Internal link resolution', () => {
9
+ const UPDATE_ORDER_STATES_PAGE =
10
+ '/docs/commerce/test-cc-orders-api/guide/update-order-states.html';
11
+ const QUICK_START_PAGE = '/docs/commerce/test-cc-orders-api/guide/quick-start.html';
12
+ const REFERENCE_PAGE =
13
+ '/docs/success/sfdocs/references/markdown-cheatsheet/markdown-cheatsheet.html#how-to-link-between-overviews-guides-and-references';
14
+
15
+ let page: GuidePage;
16
+
17
+ it('should resolve relative guide link correctly', async () => {
18
+ page = new GuidePage(UPDATE_ORDER_STATES_PAGE);
19
+ page.open();
20
+ await page.waitPageLoad();
21
+
22
+ const ordersApiLink = await $('a.internal-link-guide[href*="orders-api-overview"]');
23
+ await ordersApiLink.waitForDisplayed();
24
+ const href = await ordersApiLink.getAttribute('href');
25
+ expect(href).not.toMatch(/\.md(?:\?|#|$)/);
26
+ expect(href).toContain('orders-api-overview.html');
27
+ });
28
+
29
+ it('should resolve guide linking in same folder correctly', async () => {
30
+ page = new GuidePage(QUICK_START_PAGE);
31
+ page.open();
32
+ await page.waitPageLoad();
33
+
34
+ await (await $('>>> doc-heading[hash="see-also"]')).waitForDisplayed();
35
+
36
+ const understandTheBasicsLink = await $('=Understand the Basics');
37
+ await understandTheBasicsLink.waitForDisplayed();
38
+ const href = await understandTheBasicsLink.getAttribute('href');
39
+ expect(href).not.toMatch(/\.md(?:\?|#|$)/);
40
+ expect(href).toContain('understand-the-basics.html');
41
+ });
42
+
43
+ it('should have html link for reference to guide linking', async () => {
44
+ page = new GuidePage(REFERENCE_PAGE);
45
+ page.open();
46
+ await page.waitPageLoad();
47
+
48
+ await (
49
+ await $('>>> doc-heading[hash="how-to-link-between-overviews-guides-and-references"]')
50
+ ).waitForDisplayed();
51
+
52
+ const threeContentTypesLink = await $('=three content types');
53
+ await threeContentTypesLink.waitForDisplayed();
54
+ const href = await threeContentTypesLink.getAttribute('href');
55
+ expect(href).toContain('guide');
56
+ expect(href).toMatch(/\.html(?:#|$|\?)/);
57
+ });
58
+
59
+ it('should treat external links as external', async () => {
60
+ page = new GuidePage(REFERENCE_PAGE);
61
+ page.open();
62
+ await page.waitPageLoad();
63
+
64
+ const externalLinks = await $$('a.external-link-guide');
65
+ expect(externalLinks.length).toBeGreaterThan(0);
66
+ for (const link of externalLinks) {
67
+ expect(await link.getAttribute('target')).toBe('_blank');
68
+ }
69
+ });
70
+ });
@@ -7,7 +7,7 @@ import Page from '../pageobjects/page';
7
7
  * @param givenPage : This is the current page for which we need to test chrome.
8
8
  */
9
9
 
10
- export default function layoutTest(givenPage: Page, headerTag = 'dx-header') {
10
+ export default function layoutTest(givenPage: Page, headerTag = 'dx-global-header') {
11
11
  let page: Page;
12
12
  describe('Basic layout testing > ', () => {
13
13
  beforeEach(async () => {
@@ -15,10 +15,12 @@ export default function layoutTest(givenPage: Page, headerTag = 'dx-header') {
15
15
  });
16
16
 
17
17
  it('should display header', async () => {
18
- const header = await $('hgf-c360contextnav');
18
+ const header = await $(headerTag);
19
19
  await header.waitForDisplayed();
20
20
  expect(await header.isDisplayed()).toBeTruthy();
21
- expect(await header.getText()).toContain('Developers');
21
+ if (headerTag === 'dx-global-header') {
22
+ expect(await header.getText()).toContain('Developers');
23
+ }
22
24
  });
23
25
 
24
26
  it.skip('should display global search box', async () => {
@@ -0,0 +1,94 @@
1
+ import GuidePage from '../pageobjects/guide.page';
2
+
3
+ let page: GuidePage;
4
+ let liquidEnabled = false;
5
+
6
+ describe('Liquid Template Rendering', () => {
7
+ before(async () => {
8
+ try {
9
+ page = new GuidePage('/docs/liquid-project/guide/liquid-quickstart.html');
10
+ page.open();
11
+ await page.waitPageLoad();
12
+
13
+ const content = await $('>>>doc-content-layout');
14
+ const html = await content.getHTML();
15
+ const text = await content.getText();
16
+
17
+ const hasLiquidOutput = text.includes('Hello from data.yaml');
18
+
19
+ const hasRawSyntax =
20
+ html.includes('{{ data.greeting }}') ||
21
+ html.includes('{{ topic }}') ||
22
+ (html.includes('{{') && html.includes('{%'));
23
+
24
+ if (hasRawSyntax) {
25
+ liquidEnabled = false;
26
+ } else if (hasLiquidOutput) {
27
+ liquidEnabled = true;
28
+ } else {
29
+ liquidEnabled = false;
30
+ }
31
+ } catch (e) {
32
+ liquidEnabled = false;
33
+ }
34
+ });
35
+
36
+ beforeEach(async () => {
37
+ if (liquidEnabled) {
38
+ page = new GuidePage('/docs/liquid-project/guide/liquid-quickstart.html');
39
+ page.open();
40
+ await page.waitPageLoad();
41
+ }
42
+ });
43
+
44
+ it('Should render Liquid variables from frontmatter and data.yaml', async function () {
45
+ if (!liquidEnabled) {
46
+ this.skip();
47
+ }
48
+
49
+ const content = await $('>>>doc-content-layout');
50
+ const text = await content.getText();
51
+
52
+ expect(text.includes('GitHub for writers')).toBeTruthy();
53
+ expect(text.includes('Hello from data.yaml')).toBeTruthy();
54
+
55
+ const html = await content.getHTML();
56
+ expect(html.includes('{{ topic }}')).toBeFalsy();
57
+ expect(html.includes('{{ data.greeting }}')).toBeFalsy();
58
+ });
59
+
60
+ it('Should render included templates from shared directory', async function () {
61
+ if (!liquidEnabled) {
62
+ this.skip();
63
+ }
64
+
65
+ const content = await $('>>>doc-content-layout');
66
+ const html = await content.getHTML();
67
+
68
+ expect(html.includes('AccountId') || html.includes('Type:')).toBeTruthy();
69
+ expect(html.includes('Properties:')).toBeTruthy();
70
+ expect(html.includes('Special Access Rules')).toBeTruthy();
71
+ expect(html.includes('{% include')).toBeFalsy();
72
+ });
73
+
74
+ it('Should render loops over data arrays', async function () {
75
+ if (!liquidEnabled) {
76
+ this.skip();
77
+ }
78
+
79
+ const content = await $('>>>doc-content-layout');
80
+ const html = await content.getHTML();
81
+ const text = await content.getText();
82
+
83
+ expect(text.includes('create()')).toBeTruthy();
84
+ expect(text.includes('query()')).toBeTruthy();
85
+ expect(text.includes('update()')).toBeTruthy();
86
+
87
+ const listItems = await content.$$('li');
88
+ expect(listItems.length).toBeGreaterThan(5);
89
+
90
+ expect(html.includes('{% for')).toBeFalsy();
91
+ expect(html.includes('{%- for')).toBeFalsy();
92
+ expect(html.includes('{% endfor')).toBeFalsy();
93
+ });
94
+ });
@@ -9,6 +9,7 @@ describe('API Pages', () => {
9
9
  beforeEach(async () => {
10
10
  referencePage = new ReferencePage('/docs/commerce/test-cc-orders-api/references');
11
11
  referencePage.open();
12
+ await referencePage.waitForSpecBasedReferencePageLoad();
12
13
  });
13
14
 
14
15
  layoutTest(referencePage, 'doc-header');
@@ -85,6 +86,7 @@ describe('Reference Versioning', () => {
85
86
  beforeEach(async () => {
86
87
  referencePage = new ReferencePage('/docs/c-cart-apis/references/ref1?meta=Summary');
87
88
  referencePage.open();
89
+ await referencePage.waitForSpecBasedReferencePageLoad();
88
90
  });
89
91
 
90
92
  it('should check for the presence of version picker dropdown', async () => {
@@ -133,7 +135,7 @@ describe('Reference Versioning', () => {
133
135
 
134
136
  await versionOptions[1].waitForClickable();
135
137
  await versionOptions[1].click();
136
- await browser.pause(1000);
138
+ await referencePage.waitForSpecBasedReferencePageLoad();
137
139
 
138
140
  const apiTitle = await (await $('>>>div.api-title span')).getText();
139
141
  const apiVersion = await (await $('>>>p.version span')).getText();
@@ -179,8 +181,10 @@ describe('Reference Versioning', () => {
179
181
 
180
182
  await referencePage.waitForSpecBasedReferencePageLoad();
181
183
  await referencePage.selectVersion();
184
+ await referencePage.waitForSpecBasedReferencePageLoad();
182
185
 
183
186
  const container = await $('>>>div.title-area div');
187
+ await container.waitForDisplayed();
184
188
  const apiTitle = await container.getText();
185
189
 
186
190
  expect(apiTitle).toEqual('createBasket');
@@ -274,6 +278,7 @@ describe('Spec-Based Flexible References', () => {
274
278
  await referencePage.waitForSpecBasedReferencePageLoad();
275
279
 
276
280
  const container = await $('>>>div.title-area div');
281
+ await container.waitForDisplayed();
277
282
  const apiTitle = await container.getText();
278
283
 
279
284
  expect(apiTitle).toEqual('getBasket');
@@ -0,0 +1,49 @@
1
+ /**
2
+ * WebdriverIO configuration for E2E tests on Static Portal Pages (SSG) on DSC
3
+ *
4
+ * Prerequisites:
5
+ * 1. Build static pages: yarn build:static:portal
6
+ * 2. Start static server: yarn start:static:portal (runs on port 3000)
7
+ * 3. Run tests: yarn test:e2e:static:portal
8
+ */
9
+ const BASE_CONFIG = require('./common-wdio.conf');
10
+
11
+ exports.config = {
12
+ ...BASE_CONFIG.config,
13
+ // Static pages are served on port 3000 by default; override via E2E_BASE_URL env var
14
+ baseUrl: process.env.E2E_BASE_URL || 'http://localhost:3000',
15
+
16
+ // Reuse the same portal specs - static pages have the same URL structure
17
+ specs: ['./src/portal/specs/**/*.test.ts', './src/shared/specs/*.test.ts'],
18
+
19
+ // Exclude tests that require dynamic server features or pages not in static build
20
+ exclude: [
21
+ './src/portal/specs/docs.api.test.ts',
22
+ './src/portal/specs/docs.test.ts',
23
+ './src/shared/specs/banner.test.ts',
24
+ ],
25
+
26
+ // Output directory for static test results
27
+ outputDir: './_results_/static-portal',
28
+
29
+ // Override reporters to use separate output directories
30
+ reporters: [
31
+ 'spec',
32
+ [
33
+ require('wdio-video-reporter'),
34
+ {
35
+ saveAllVideos: false,
36
+ videoSlowdownMultiplier: 3,
37
+ outputDir: './_results_/static-portal/videos',
38
+ },
39
+ ],
40
+ [
41
+ 'allure',
42
+ {
43
+ outputDir: './_results_/static-portal/allure-raw',
44
+ disableWebdriverStepsReporting: true,
45
+ disableWebdriverScreenshotsReporting: false,
46
+ },
47
+ ],
48
+ ],
49
+ };
@@ -24,13 +24,6 @@ exports.config = {
24
24
  './src/portal/specs/docs.test.ts',
25
25
  ],
26
26
 
27
- // Skip footer tests - footer is not available in static builds
28
- mochaOpts: {
29
- ...BASE_CONFIG.config.mochaOpts,
30
- grep: 'should display footer',
31
- invert: true,
32
- },
33
-
34
27
  // Output directory for static test results
35
28
  outputDir: './_results_/static-portal',
36
29