-
Couldn't load subscription status.
- Fork 26
Update Broken link Script #3808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
d244563
fc7b046
2a8d7d8
1a94888
faf4296
f11e465
72cbea4
a07f658
e79ac2b
4dbc2f3
a3544de
b5c4efe
f34db0e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,41 +5,52 @@ describe('Link and Routing Validation - Individual URL Checks', () => { | |||||||
| const totalTests = urls.length; | ||||||||
|
|
||||||||
|
|
||||||||
| const escRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||||||||
| const expectFragmentExists = (doc, fragment) => { | ||||||||
| const decodedFragment = decodeURIComponent(fragment); | ||||||||
| const collectFragments = (root) => { | ||||||||
| const ids = Array.from(root.querySelectorAll('[id]')).map(el => el.id); | ||||||||
| const names = Array.from(root.querySelectorAll('a[name]')).map(a => a.getAttribute('name')); | ||||||||
| return [...ids, ...names].filter(Boolean); | ||||||||
| }; | ||||||||
|
|
||||||||
| const expectFragmentExists = (htmlContent, fragment) => { | ||||||||
| const escFragment = escRegExp(fragment); | ||||||||
| const regex = new RegExp(`(id=["']${escFragment}["']|name=["']${escFragment}["'])`); | ||||||||
| const exists = regex.test(htmlContent); | ||||||||
| expect(exists, `An element with id or name "${fragment}" should exist in HTML`).to.be.true; | ||||||||
| let allFragments = collectFragments(doc); | ||||||||
| const iframes = doc.querySelectorAll('iframe, frame'); | ||||||||
| for (const frame of iframes) { | ||||||||
| try { | ||||||||
| const frameDoc = frame.contentDocument || frame.contentWindow?.document; | ||||||||
| if (frameDoc) { | ||||||||
| allFragments = allFragments.concat(collectFragments(frameDoc)); | ||||||||
| } | ||||||||
| } catch (e) { | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| const exists = allFragments.some(f => f === decodedFragment); | ||||||||
|
|
||||||||
| if (!exists) { | ||||||||
| cy.log(`Available fragments (including frames):\n${allFragments.join('\n')}`); | ||||||||
| } | ||||||||
|
|
||||||||
| expect(exists, `An element with id or name = "#${fragment}" should exist in HTML or frames`).to.be.true; | ||||||||
| }; | ||||||||
|
|
||||||||
| const expectNoUnencodedParentheses = (url) => { | ||||||||
| cy.wrap(url).should('not.match', /[()]/, `URL should not contain unencoded parentheses: ${url}`); | ||||||||
| }; | ||||||||
|
|
||||||||
| // Note: On GitHub pages, heading IDs are prefixed with "user-content-". | ||||||||
| const checkGithubFragment = (fragment) => { | ||||||||
| const normalizedFragment = fragment.toLowerCase().replace(/[^\w\-]+/g, '-').replace(/^-+|-+$/g, ''); | ||||||||
| cy.document().then((doc) => { | ||||||||
| const anchorExists = Array.from(doc.querySelectorAll('a')) | ||||||||
| .some(a => a.getAttribute('href') === `#${normalizedFragment}`); | ||||||||
|
|
||||||||
| expect(anchorExists, `Fragment "#${normalizedFragment}" should exist in href attribute of an <a> tag`).to.be.true; | ||||||||
| const allFragments = Array.from(doc.querySelectorAll('a')) | ||||||||
| .map(a => a.getAttribute('href')) | ||||||||
| .filter(href => href && href.startsWith('#')); | ||||||||
| cy.log(`Available fragments on page:\n${allFragments.join('\n')}`); | ||||||||
| const ids = Array.from(doc.querySelectorAll('[id]')).map(el => el.id.replace(/^user-content-/, '')); | ||||||||
| cy.log(`Available GitHub IDs:\n${ids.join('\n')}`); | ||||||||
| const exists = ids.some(id => id === fragment); | ||||||||
| expect(exists, `Fragment "#${fragment}" should exist in GitHub page`).to.be.true; | ||||||||
| }); | ||||||||
| }; | ||||||||
|
|
||||||||
| const checkRegularFragment = (fragment) => { | ||||||||
| cy.document().then((doc) => { | ||||||||
| const html = doc.documentElement.innerHTML; | ||||||||
| expectFragmentExists(html, fragment); | ||||||||
| const ids = Array.from(doc.querySelectorAll('[id]')).map(el => el.id); | ||||||||
| const names = Array.from(doc.querySelectorAll('a[name]')).map(a => a.getAttribute('name')); | ||||||||
| const allFragments = [...ids, ...names]; | ||||||||
| cy.log(`Available elements on page with ids and names:\n${allFragments.join('\n')}`); | ||||||||
| expectFragmentExists(doc, fragment); | ||||||||
| }); | ||||||||
| }; | ||||||||
|
|
||||||||
|
|
@@ -62,17 +73,37 @@ describe('Link and Routing Validation - Individual URL Checks', () => { | |||||||
|
|
||||||||
| const hasNonHtmlExtension = nonHtmlExtensions.some(ext => url.endsWith(ext)); | ||||||||
| const isNonHtmlResource = hasNonHtmlExtension || url.includes('/files/') || url.includes('/downloads/'); | ||||||||
| const isNpmPackagePage = url.startsWith('https://www.npmjs.com/package/'); | ||||||||
| if (isNpmPackagePage) { | ||||||||
| const m = url.match(/^https:\/\/www\.npmjs\.com\/package\/(@[^/]+\/[^#?]+)/); | ||||||||
| const pkg = m ? m[1] : null; | ||||||||
| const encodedUrl = pkg ? url.replace(pkg, encodeURIComponent(pkg)) : url; | ||||||||
|
|
||||||||
| if (pkg) { | ||||||||
| cy.request({ | ||||||||
| url: `https://registry.npmjs.org/${pkg}`, | ||||||||
| failOnStatusCode: false, | ||||||||
| headers: { Accept: 'application/vnd.npm.install-v1+json' } | ||||||||
| }).then((res) => { | ||||||||
| expect(res.status, `npm registry status for ${pkg}`).to.eq(200); | ||||||||
| }); | ||||||||
| } | ||||||||
| cy.visit(encodedUrl, { timeout: 50000, failOnStatusCode: false }); | ||||||||
| cy.url().should('include', '/package/%40'); | ||||||||
|
Comment on lines
+78
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain this logic? I see you match only packages with scoped names starting with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently, I am matching only scoped packages (@scope/pkg) since those are the only ones we have in our docs. The registry call is used to confirm that the package actually exists (to avoid npmjs.com’s bot-blocks). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
ok, that's clear
Sorry, didn't get that. If you navigate to the encoded URL, then it's expected that you detect cy.visit('https://www.npmjs.com/package/%40c8y/made-up-package', { timeout: 50000, failOnStatusCode: false });
cy.url().should('include', `/package/%40`);Isn't the registry check enough? |
||||||||
| completedTests++; | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| Cypress.env('sourceFiles', item.files); | ||||||||
| expectNoUnencodedParentheses(url); | ||||||||
|
|
||||||||
| if (isNonHtmlResource) { | ||||||||
| cy.log(`Validating non-HTML resource: ${url}`); | ||||||||
| cy.request({ | ||||||||
| url: url, | ||||||||
| failOnStatusCode: false | ||||||||
| }).then((response) => { | ||||||||
| expect(response.status).to.be.oneOf([200, 304]); | ||||||||
| expect(response.status).to.be.oneOf([200, 201, 202, 203, 204, 301, 302, 304]); | ||||||||
|
|
||||||||
| if (url.endsWith('.json')) { | ||||||||
| expect(response.body).to.be.an('object'); | ||||||||
|
|
@@ -117,12 +148,23 @@ describe('Link and Routing Validation - Individual URL Checks', () => { | |||||||
| checkGithubFragment(fragment); | ||||||||
| } | ||||||||
| else if (fragment) { | ||||||||
| cy.visit(url, { timeout: 20000 }); | ||||||||
| cy.visit(url, { timeout: 30000, failOnStatusCode: false, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/122.0 Safari/537.36' }}); | ||||||||
| checkRegularFragment(fragment); | ||||||||
| } | ||||||||
| else { | ||||||||
| cy.visit(url, { timeout: 20000 }); | ||||||||
| cy.document().its('body').should('not.be.empty'); | ||||||||
| cy.request({ | ||||||||
| url: url, | ||||||||
| failOnStatusCode: false | ||||||||
| }).then((response) => { | ||||||||
| const contentType = response.headers['content-type'] || ''; | ||||||||
| if (!contentType.includes('text/html')) { | ||||||||
| cy.log(`Non-HTML content detected for ${url}, skipping cy.visit()`); | ||||||||
| expect(response.status).to.be.oneOf([200, 201, 202, 203, 204, 301, 302, 304]); | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we check that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For example, Link: https://download.cumulocity.com/Apama/Debian/. These are file repositories or download links, not HTML pages, so I only check that they return a valid status and skip checking the body. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This link seems to be special as it actually returns 2
Anyway, I'd consider checking
Suggested change
|
||||||||
| } else { | ||||||||
| cy.visit(url, { timeout: 20000 }); | ||||||||
| cy.document().its('body').should('not.be.empty'); | ||||||||
| } | ||||||||
| }); | ||||||||
| } | ||||||||
|
|
||||||||
| completedTests++; | ||||||||
|
|
||||||||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give me example of the case you're addressing here? I've made some tests and if you have a document with iframe, and iframe contains an anchor, you cannot use url like
/main-document.html#a-name-from-iframeto link to the anchor inside the iframe, i.e. user won't be scrolled to the right position within the iframe.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intention here isn’t to support direct navigation to iframe anchors via #fragment, but to verify that referenced anchors (even inside embedded documents) actually exist. We have some pages that load documentation or content in iframes, so this logic helps our validation detect missing anchors there too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be easier if you provided an example link and use case. Sure, with the above mechanism we can verify that the anchor exists inside the iframe on the target page (if they are from the same domain). My concern is also user experience in case we have a link in our docs which refers to a specific anchor inside an iframe on another page (internal or external), then user won't be navigated to the referred section directly, but just to the top of the page.