Skip to content

Commit b0f9c1b

Browse files
06180618
andauthored
test(docs): add E2E test to check the broken links at build time (#2844)
* feat(docs): setup e2e test * test: add sitemap link test * test: links on each page * test: fix headless tests * test: test amplify yml add pm2 and memory * test: fix pm2 start and max-old-space-size * test: remove node memory increase * test: fix pm2 and test links on each page * test: add iterate to 3 * test: seperate and iterate 3 times * test: remove cy wrap * test: reduce to 2 pages * test: skip links included in sitemap * test: only test sitemap links * test: split to 2 files * test: split and reduce to 30 * test: split and reduce to 20 * test: test all links on sitemap * test: all links on all pages * test: use yarn doc build * test: add followRedirect * test: fix amplify yml and status * test: fix amplify yml npm start * test: fix amplify yml change to npm start * test: fix amplify yml npm run build * test: fix amplify yml node 16 * test: fix amplify yml remove mocha version * test: fix amplify yml npm ci * test: fix amplify yml change npm ci to npm install * test: fix amplify yml add cypress install * test: fix amplify yml change install order * test: fix amplify yml remove mocha version, add npm install * test: fix amplify yml remove mocha version, add npm install, cypress install * test(amplify.yml): cypress install -> npm install cypress * test(docs): first 10 pages * test(docs): test 10-14 * test(docs): test 15-19 * test(docs): test 20-30 * test(docs): test 30-40 * test(docs): test 40-50 * test(docs): test 50-60 * test(docs): test 60-70 * test(docs): test 70-80 * test(docs): test 80-90 * test(docs): test 90-100 * test(docs): test 100-110 * test(docs): all broken link tests * test(docs): clearLocalStorage and skip requested path * test(docs): add max-old-space-size * test(docs): broken link skip testing hash * test(docs): broken link test remove max-old-space-size * test(docs): broken link test add max-old-space-size * test(docs): broken link test add requestedLinks before requesting * test(docs): broken link test use HEAD request * test(docs): broken link test code clean up * test(docs): broken link test remove support files and misc change * test(docs): broken link test remove button tag assertions * test(docs): broken link test change numTestsKeptInMemory to 1 * test(docs): broken link test use it.each * test(docs): broken link move type to top and cleanup commented code * test(docs): broken link move hrefWorks and logMsg utls out * fix(docs): broken links each i undefined Co-authored-by: 0618 <[email protected]>
1 parent 7dbf0bc commit b0f9c1b

File tree

26 files changed

+728
-143
lines changed

26 files changed

+728
-143
lines changed

amplify.yml

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
version: 1
22
applications:
3-
- frontend:
3+
- appRoot: docs
4+
frontend:
45
phases:
56
preBuild:
67
commands:
@@ -13,7 +14,7 @@ applications:
1314
- export PATH="$PATH:${FLUTTER_HOME}/bin"
1415
# Skip cypress binary installation, as it's unneeded for docs and often fails transiently
1516
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
16-
- export CYPRESS_INSTALL_BINARY=0
17+
- export CYPRESS_INSTALL_BINARY=0
1718
- (cd .. && yarn install && yarn build)
1819
build:
1920
commands:
@@ -26,4 +27,31 @@ applications:
2627
baseDirectory: .next
2728
files:
2829
- '**/*'
29-
appRoot: docs
30+
test:
31+
phases:
32+
preTest:
33+
commands:
34+
- nvm install 16.13 # NodeJS 16.13+ is the latest version that Amplify Hosting supports
35+
- nvm use 16
36+
- export NODE_OPTIONS="--max-old-space-size=8192"
37+
- npm install
38+
- npm install wait-on
39+
- npm install pm2
40+
- npm install mocha mochawesome mochawesome-merge mochawesome-report-generator
41+
- npm install cypress
42+
- npm run build
43+
- npx pm2 start npm -- start
44+
- 'npx wait-on http://localhost:3000'
45+
test:
46+
commands:
47+
- 'npx cypress run --reporter mochawesome --reporter-options "reportDir=cypress/report/mochawesome-report,overwrite=false,html=false,json=true,timestamp=mmddyyyy_HHMMss"'
48+
postTest:
49+
commands:
50+
- npx mochawesome-merge cypress/report/mochawesome-report/mochawesome*.json > cypress/report/mochawesome.json
51+
- npx pm2 kill
52+
artifacts:
53+
baseDirectory: cypress
54+
configFilePath: '**/mochawesome.json'
55+
files:
56+
- '**/*.png'
57+
- '**/*.mp4'

docs/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,7 @@ yarn-error.log*
3737

3838
# vercel
3939
.vercel
40+
41+
# Cypress
42+
/cypress/videos
43+
/cypress/screenshots

docs/cypress.config.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { defineConfig } from 'cypress';
2+
import sitemapUrls from 'sitemap-urls';
3+
import { BASE_URL } from './cypress/data/constants';
4+
import fs from 'fs';
5+
6+
export default defineConfig({
7+
e2e: {
8+
setupNodeEvents(on, config) {
9+
// implement node event listeners here
10+
on('task', {
11+
readSitemapLinks: async () => {
12+
if (fs.existsSync('./public/sitemap.xml')) {
13+
const siteMapContent = fs.readFileSync(
14+
'./public/sitemap.xml',
15+
'utf8'
16+
);
17+
const sitemapLinks: string[] = await sitemapUrls.extractUrls(
18+
siteMapContent
19+
);
20+
return sitemapLinks.map((link) =>
21+
link
22+
.replace(`${BASE_URL}/`, '')
23+
.replace('https://www.dev.ui.docs.amplify.aws/', '')
24+
.replace('https://ui.docs.amplify.aws/', '')
25+
);
26+
}
27+
},
28+
log: (message) => {
29+
console.log(message);
30+
return null;
31+
},
32+
});
33+
},
34+
baseUrl: BASE_URL,
35+
numTestsKeptInMemory: 1,
36+
supportFile: false,
37+
},
38+
});

docs/cypress/data/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const BASE_URL = 'http://localhost:3000';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* REQUEST_GET_LINKS is a manually maintained list.
3+
* It contains the links which do not allow HEAD request,
4+
* so we have to make a GET request when testing.
5+
*/
6+
7+
export const REQUEST_GET_LINKS = [
8+
'https://www.figma.com/community/file/1047600760128127424',
9+
'https://www.figma.com/community/plugin/1040722185526429545/AWS-Amplify-Theme-Editor',
10+
'https://console.aws.amazon.com/console/home',
11+
];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* VALIDATED_LINKS is a manually maintained allowlist.
3+
* Any links on this list will not be tested, so KEEP THIS LIST AS SHORT AS POSSIBLE.
4+
* Links added to this list because of one of the following reasons
5+
* 1) they return unexpected status code
6+
* 2) they take too long to load
7+
*/
8+
9+
export const VALIDATED_LINKS = [
10+
'https://github.com/aws-amplify/amplify-ui/issues/new/choose',
11+
'https://twitter.com/AWSAmplify',
12+
];

docs/cypress/e2e/links.cy.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { VALIDATED_LINKS } from '../data/validatedLinks';
2+
import { REQUEST_GET_LINKS } from '../data/requestGetLinks';
3+
import { BASE_URL } from '../data/constants';
4+
import 'cypress-each';
5+
6+
type EvtName =
7+
| 'CHECKING'
8+
| 'SKIPPING_SITEMAP'
9+
| 'SKIPPING_VALIDATED'
10+
| 'REQUESTING'
11+
| 'RETURNING'
12+
| 'NO_HREF';
13+
let allLinks: string[] = [];
14+
const numberOfLinks = 119;
15+
const requestedLinks: Set<string> = new Set();
16+
17+
before(() => {
18+
cy.task('readSitemapLinks').then((links: string[]) => {
19+
allLinks = allLinks.concat(links);
20+
});
21+
});
22+
23+
describe('Local Sitemap', () => {
24+
it('should have 119 links', () => {
25+
expect(allLinks.length).to.eq(numberOfLinks);
26+
});
27+
});
28+
29+
describe(`All pages on Sitemap`, () => {
30+
it.each(numberOfLinks)('all links on page %k should work', (i: string) => {
31+
const link = allLinks[i];
32+
cy.task('log', `🧪[TESTING...] page ${BASE_URL}/${link}`);
33+
cy.visit(link || '/');
34+
cy.get('a').each((el) => hrefWorks(el, link));
35+
});
36+
});
37+
38+
export {};
39+
40+
function hrefWorks(htmlTag: JQuery<HTMLElement>, link: string): void {
41+
const tagHref: string = htmlTag.prop('href');
42+
const tagHash: string = htmlTag.prop('hash');
43+
const tagText: string = htmlTag.prop('text');
44+
const tagName: string = htmlTag.prop('tagName');
45+
let pureHref: string;
46+
if (tagHref) {
47+
pureHref = tagHref.replace(tagHash, ''); // TODO: add test to validate links with a hash tag.
48+
logMessage({ evtName: 'CHECKING', link, pureHref, tagName, tagText });
49+
50+
if (allLinks.includes(`${pureHref.replace(`${BASE_URL}/`, '')}`)) {
51+
expect(`${pureHref.replace(`${BASE_URL}/`, '')}`).to.oneOf(allLinks);
52+
logMessage({
53+
evtName: 'SKIPPING_SITEMAP',
54+
link,
55+
pureHref,
56+
tagName,
57+
tagText,
58+
});
59+
} else if (
60+
allLinks.includes(`${pureHref.replace(`${BASE_URL}/`, 'react/')}`)
61+
) {
62+
expect(`${pureHref.replace(`${BASE_URL}/`, 'react/')}`).to.oneOf(
63+
allLinks
64+
);
65+
logMessage({
66+
evtName: 'SKIPPING_SITEMAP',
67+
link,
68+
pureHref,
69+
tagName,
70+
tagText,
71+
});
72+
} else if (
73+
VALIDATED_LINKS.includes(pureHref) ||
74+
VALIDATED_LINKS.includes(`${pureHref.replace(BASE_URL, '')}`) ||
75+
requestedLinks.has(pureHref)
76+
) {
77+
logMessage({
78+
evtName: 'SKIPPING_VALIDATED',
79+
link,
80+
pureHref,
81+
tagName,
82+
tagText,
83+
});
84+
} else {
85+
const requestMethod = REQUEST_GET_LINKS.includes(pureHref)
86+
? 'GET'
87+
: 'HEAD';
88+
logMessage({
89+
evtName: 'REQUESTING',
90+
link,
91+
pureHref,
92+
tagName,
93+
tagText,
94+
});
95+
requestedLinks.add(pureHref);
96+
cy.request({
97+
url: pureHref,
98+
followRedirect: false,
99+
method: requestMethod,
100+
}).then(({ status }) => {
101+
logMessage({
102+
evtName: 'RETURNING',
103+
link,
104+
pureHref,
105+
tagName,
106+
tagText,
107+
status,
108+
});
109+
expect(status).to.oneOf([200, 301, 303]);
110+
cy.clearLocalStorage();
111+
});
112+
}
113+
} else if (tagName === 'A') {
114+
logMessage({ evtName: 'NO_HREF', link, pureHref, tagName, tagText });
115+
}
116+
}
117+
118+
function logMessage({
119+
evtName,
120+
link,
121+
pureHref,
122+
status,
123+
tagName,
124+
tagText,
125+
}: {
126+
evtName: EvtName;
127+
link: string;
128+
pureHref: string;
129+
status?: number;
130+
tagName: string;
131+
tagText: string;
132+
}) {
133+
switch (evtName) {
134+
case 'CHECKING':
135+
return cy.task(
136+
'log',
137+
`🔍[CHECKING...] ${pureHref} from ${tagName} tag ${
138+
tagText ? `"${tagText}"` : ''
139+
} on ${BASE_URL}/${link}`
140+
);
141+
case 'SKIPPING_SITEMAP':
142+
return cy.task(
143+
'log',
144+
`⏭[SKIPPING...] ${pureHref} from ${tagName} tag ${
145+
tagText ? `"${tagText}"` : ''
146+
} on ${BASE_URL}/${link} because it's included in Sitemap and already tested.`
147+
);
148+
case 'SKIPPING_VALIDATED':
149+
return cy.task(
150+
'log',
151+
`⏭[SKIPPING...] ${pureHref} from ${tagName} tag ${
152+
tagText ? `"${tagText}"` : ''
153+
} on ${BASE_URL}/${link} because it's already validated.`
154+
);
155+
case 'REQUESTING':
156+
return cy.task(
157+
'log',
158+
`📞[REQUESTING...] ${pureHref} from ${tagName} tag ${
159+
tagText ? `"${tagText}"` : ''
160+
} on ${BASE_URL}/${link}`
161+
);
162+
case 'RETURNING':
163+
return cy.task(
164+
'log',
165+
`↩️ [RETURNING STATUS...] ${status} for ${pureHref}`
166+
);
167+
case 'NO_HREF':
168+
return cy.task(
169+
'log',
170+
`⚠ ${tagName} tag ${
171+
tagText ? `"${tagText}"` : ''
172+
} on ${BASE_URL}/${link} doesn't have a href attribute`
173+
);
174+
default:
175+
break;
176+
}
177+
}

docs/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,15 @@
6363
"@types/react": "17.0.47",
6464
"autoprefixer": "^10.2.4",
6565
"canvas": "^2.9.1",
66+
"cypress": "^10.10.0",
67+
"cypress-each": "^1.11.0",
6668
"dotenv-safe": "^8.2.0",
6769
"esbuild-register": "^3.3.2",
6870
"eslint": "^7.32.0",
6971
"eslint-config-next": "12.0.10",
7072
"globby": "^13.1.1",
7173
"postcss": "^8.2.6",
74+
"sitemap-urls": "^3.0.0",
7275
"ts-morph": "^15.1.0"
7376
}
7477
}

docs/scripts/generate-sitemap-robotstxt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async function generateSitemap() {
3535
<?xml version="1.0" encoding="UTF-8"?>
3636
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
3737
<url>
38-
<loc>${process.env.SITE_URL}</loc>
38+
<loc>${process.env.SITE_URL}/</loc>
3939
<changefreq>weekly</changefreq>
4040
<priority>0.5</priority>
4141
<lastmod>2022-05-19T16:24:03.254Z</lastmod>

docs/src/data/globalnav.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const RIGHT_NAV_LINKS = [
22
{
33
type: 'EXTERNAL',
44
label: 'Pricing',
5-
url: 'https://aws.amazon.com/amplify/pricing',
5+
url: 'https://aws.amazon.com/amplify/pricing/',
66
order: 6,
77
},
88
{
@@ -23,7 +23,7 @@ export const LEFT_NAV_LINKS = [
2323
{
2424
type: 'DEFAULT',
2525
label: 'Learn',
26-
url: 'https://amplify.aws/learn',
26+
url: 'https://amplify.aws/learn/',
2727
order: 2,
2828
},
2929
{
@@ -38,7 +38,7 @@ export const SOCIAL_LINKS = [
3838
{
3939
type: 'ICON',
4040
label: 'Discord',
41-
url: 'https://discord.com/invite/amplify',
41+
url: 'https://discord.com/invite/amplify/',
4242
order: 8,
4343
icon: 'DISCORD',
4444
},

0 commit comments

Comments
 (0)