Merge pull request #5675 from ayshiff/feature/techdocs-e2e

Feature/techdocs e2e
This commit is contained in:
Patrik Oldsberg
2021-09-29 13:46:13 +02:00
committed by GitHub
27 changed files with 864 additions and 20 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-techdocs': patch
'@backstage/plugin-user-settings': patch
---
Add "data-testid" for e2e tests and fix techdocs entity not found error.
+1
View File
@@ -23,3 +23,4 @@
/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining
/.changeset/search-* @backstage/techdocs-core
/.changeset/techdocs-* @backstage/techdocs-core
/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core
+3
View File
@@ -133,3 +133,6 @@ site
# Sensitive credentials
*-credentials.yaml
# e2e tests
cypress/cypress/*
+3 -1
View File
@@ -262,7 +262,9 @@ catalog:
# Backstage example groups and users
- type: file
target: ../catalog-model/examples/acme-corp.yaml
# Backstage end-to-end tests of TechDocs
- type: file
target: ../../cypress/e2e-fixture.catalog.info.yaml
scaffolder:
# Use to customize default commit author info used when new components are created
# defaultAuthor:
+1
View File
@@ -11,6 +11,7 @@ module.exports = {
bundledDependencies: false,
},
],
'jest/valid-expect': 'off',
'jest/expect-expect': 'off',
'no-restricted-syntax': 'off',
},
+4 -2
View File
@@ -2,7 +2,9 @@
"baseUrl": "http://localhost:7000",
"integrationFolder": "./src/integration",
"supportFile": "./src/support",
"fixturesFolder": "./src/fixures",
"fixturesFolder": "./src/fixtures",
"pluginsFile": "./src/plugins",
"defaultCommandTimeout": 10000
"defaultCommandTimeout": 10000,
"viewportHeight": 900,
"viewportWidth": 1440
}
+11
View File
@@ -0,0 +1,11 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: techdocs-e2e-fixture
description: Used for end-to-end tests of TechDocs in Backstage.
annotations:
backstage.io/techdocs-ref: dir:./fixtures
spec:
type: service
lifecycle: experimental
owner: user:guest
+3
View File
@@ -0,0 +1,3 @@
# Home page
This is a basic documentation used for end-to-end tests.
+109
View File
@@ -0,0 +1,109 @@
# Sub-page 1
## Section 1.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 1.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 1.3
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
+121
View File
@@ -0,0 +1,121 @@
# Sub-page 3
## Section 3.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 3.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
### Sub-Section 3.2.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
### Sub-Section 3.2.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 3.3
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
+146
View File
@@ -0,0 +1,146 @@
# Sub-page 2
## Section 2.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 2.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
### Sub-Section 2.2.1
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
### Sub-Section 2.2.2
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
## Section 2.3
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
To next page!
[Link to Section 1.1](sub-page-one.md#section-11)
+10
View File
@@ -0,0 +1,10 @@
site_name: e2e Fixture Documentation
site_description: Documentation used for end-to-end tests of TechDocs in Backstage.
nav:
- Home: index.md
- Sub-page 1: sub-page-one.md
- Sub-page 2: sub-page-two.md
- Nested Sub-pages:
- Sub-page 3: sub-page-three.md
plugins:
- techdocs-core
@@ -0,0 +1,188 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types="cypress" />
import 'os';
describe('TechDocs', () => {
beforeEach(() => {
cy.loginAsGuest();
cy.mockSockJSNode();
cy.interceptTechDocsAPICalls();
});
describe('Navigating to TechDocs', () => {
it('should navigate to the TechDocs home page via the primary navigation bar', () => {
cy.visit('/');
cy.wait(500);
cy.get('[data-testid="sidebar-root"]')
.get('div')
.get('a[href="/docs"]')
.click();
cy.contains('Documentation');
});
it('should navigate to the TechDocs home page from the URL', () => {
cy.visit('/docs');
cy.wait(500);
cy.contains('Documentation');
});
it('should navigate to a specific TechDocs entity from the "Overview" tab', () => {
cy.visit('/docs');
cy.contains('techdocs-e2e-fixture')
.parents()
.eq(2)
.contains('Read Docs')
.click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/docs/default/Component/techdocs-e2e-fixture',
);
});
});
it('should navigate to a specific TechDocs entity page from a URL', () => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture');
cy.waitHomePage();
cy.contains('e2e Fixture Documentation');
cy.contains(
'Documentation used for end-to-end tests of TechDocs in Backstage.',
);
cy.getTechDocsShadowRoot().contains('Home page');
});
it('should navigate to a specific TechDocs section from a URL', () => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture/sub-page-two');
cy.waitSectionTwoPage();
cy.window().its('scrollY').should('equal', 0);
cy.getTechDocsShadowRoot().within(() => {
cy.contains('Sub-page 2');
});
});
it('should navigate to a specific TechDocs fragment from a URL', () => {
cy.visit(
'/docs/default/Component/techdocs-e2e-fixture/sub-page-two#section-23',
);
cy.waitSectionTwoPage();
// This is used to test the post-render behavior of the techdocs Reader
cy.wait(500);
cy.getTechDocsShadowRoot().within(() => {
cy.isInViewport('#section-23');
});
});
it('should navigate to a wrong TechDocs entity page from a URL', () => {
cy.visit('/docs/default/Component/wrong-component');
cy.get('[data-testid=error]').should('be.visible');
});
});
describe('Navigating within TechDocs', () => {
it('should navigate to a specific TechDocs page via the navigation bar', () => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture');
cy.waitHomePage();
cy.getTechDocsShadowRoot().within(() => {
cy.getTechDocsNavigation()
.find('> div > div > [data-md-level="0"] > ul > li:nth-child(2) > a')
.click();
cy.contains('Sub-page 1');
cy.window().its('scrollY').should('eq', 0);
});
});
describe('Navigating within a TechDocs page', () => {
beforeEach(() => {
cy.visit('/docs/default/Component/techdocs-e2e-fixture/sub-page-two');
cy.waitSectionTwoPage();
});
it('should navigate to a specific fragment within the page via the table of contents - Level 1', () => {
return cy.getTechDocsShadowRoot().within(() => {
// Section 3
cy.getTechDocsTableOfContents().within(() => {
cy.get('> div > div > nav > ul > li:nth-child(3) > a').click();
});
cy.isInViewport('#section-23');
});
});
it('should navigate to a specific fragment within the page via the table of contents - Level 2', () => {
return cy.getTechDocsShadowRoot().within(() => {
cy.isNotInViewport('#sub-section-222');
// Section 2.2
cy.getTechDocsTableOfContents()
.find(
'> div > div > nav > ul > li:nth-child(2) > nav > ul > li:nth-child(2) > a',
)
.click();
cy.isInViewport('#sub-section-222');
});
});
it('should navigate to a specific TechDocs page fragment from a link', () => {
return cy.getTechDocsShadowRoot().within(() => {
cy.get('.md-content > article')
.contains('Link to Section 1.1')
.click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/docs/default/Component/techdocs-e2e-fixture/sub-page-one/',
);
expect(loc.hash).to.eq('#section-11');
});
});
});
it('should navigate to the next page within a TechDocs entity', () => {
return cy.getTechDocsShadowRoot().within(() => {
cy.get('.md-footer-nav__link--next').click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/docs/default/Component/techdocs-e2e-fixture/sub-page-three/',
);
});
});
});
it('should navigate to the previous page within a TechDocs entity', () => {
return cy.getTechDocsShadowRoot().within(() => {
cy.get('.md-footer-nav__link--prev').click();
cy.location().should(loc => {
expect(loc.pathname).to.eq(
'/docs/default/Component/techdocs-e2e-fixture/sub-page-one/',
);
});
});
});
});
});
});
@@ -0,0 +1,31 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types="cypress" />
import 'os';
describe('Login', () => {
it('should render the login page', () => {
cy.visit('/');
cy.contains('Select a sign-in method');
});
it('should be able to login', () => {
cy.get('button').contains('Enter').click();
cy.url().should('include', '/catalog');
cy.contains('artist-lookup');
});
});
@@ -0,0 +1,35 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/// <reference types="cypress" />
import 'os';
describe('Logout', () => {
before(() => {
cy.loginAsGuest();
});
it('should be able to logout', () => {
cy.visit('/settings');
cy.get('[data-testid="user-settings-menu"]').click();
return cy
.get('[data-testid="sign-out"]')
.click()
.then(() => {
return expect(
localStorage.getItem('@backstage/core:SignInPage:provider'),
).to.be.null;
});
});
});
+116
View File
@@ -0,0 +1,116 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable jest/no-standalone-expect */
/// <reference types="cypress" />
import 'os';
Cypress.Commands.add('loginAsGuest', () => {
cy.visit('/', {
onLoad: (win: Window) =>
win.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest'),
});
});
Cypress.Commands.add('getTechDocsShadowRoot', () => {
cy.get('[data-testid="techdocs-content-shadowroot"]').shadow();
});
Cypress.Commands.add('isNotInViewport', element => {
cy.get(element).then($el => {
const bottom = Cypress.config(`viewportHeight`);
const rect = $el[0].getBoundingClientRect();
if (bottom) {
expect(rect.top).to.be.greaterThan(bottom);
expect(rect.bottom).to.be.greaterThan(bottom);
expect(rect.top).to.be.greaterThan(bottom);
expect(rect.bottom).to.be.greaterThan(bottom);
}
});
});
Cypress.Commands.add('isInViewport', element => {
cy.get(element).then($el => {
const bottom = Cypress.config(`viewportHeight`);
const rect = $el[0].getBoundingClientRect();
if (bottom) {
expect(rect.top).not.to.be.greaterThan(bottom);
expect(rect.bottom).not.to.be.greaterThan(bottom);
expect(rect.top).not.to.be.greaterThan(bottom);
expect(rect.bottom).not.to.be.greaterThan(bottom);
}
});
});
Cypress.Commands.add('getTechDocsTableOfContents', () => {
cy.get('[data-md-component="toc"]');
});
Cypress.Commands.add('getTechDocsNavigation', () => {
cy.get('[data-md-component="navigation"]');
});
Cypress.Commands.add('mockSockJSNode', () => {
cy.intercept('GET', '**/sockjs-node/info**', {
body: {
websocket: true,
origins: ['*:*'],
cookie_needed: false,
entropy: 2882389500,
},
});
});
Cypress.Commands.add('interceptTechDocsAPICalls', () => {
cy.intercept(
'GET',
'**/techdocs/metadata/entity/default/Component/techdocs-e2e-fixture',
).as('entityMetadata');
cy.intercept(
'GET',
'**/techdocs/metadata/techdocs/default/Component/techdocs-e2e-fixture',
).as('techdocsMetadata');
cy.intercept(
'GET',
'**/techdocs/sync/default/Component/techdocs-e2e-fixture',
).as('syncEntity');
cy.intercept(
'GET',
'**/techdocs/static/docs/default/Component/techdocs-e2e-fixture/sub-page-two/index.html',
).as('sectionTwoHTML');
cy.intercept(
'GET',
'**/techdocs/static/docs/default/Component/techdocs-e2e-fixture/index.html',
).as('homeHTML');
});
Cypress.Commands.add('waitSectionTwoPage', () => {
cy.wait([
'@entityMetadata',
'@syncEntity',
'@techdocsMetadata',
'@sectionTwoHTML',
]);
});
Cypress.Commands.add('waitHomePage', () => {
cy.wait(['@entityMetadata', '@syncEntity', '@techdocsMetadata', '@homeHTML']);
});
+1 -9
View File
@@ -14,12 +14,4 @@
* limitations under the License.
*/
/// <reference types="cypress" />
Cypress.Commands.add('loginAsGuest', () => {
cy.visit('/', {
onLoad: (win: Window) =>
win.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest'),
});
});
export {};
import './commands';
+50
View File
@@ -22,5 +22,55 @@ declare namespace Cypress {
* @example cy.loginAsGuests
*/
loginAsGuest(): Chainable<Element>;
/**
* Get the TechDocs shadow root element
* @example cy.getTechDocsShadowRoot
*/
getTechDocsShadowRoot(): Chainable<Element>;
/**
* Mock TechDocs backend API
* @example cy.mockTechDocs
*/
mockTechDocs(): void;
/**
* Get the TechDocs table of contents element
* @example cy.getTechDocsShadowRoot
*/
getTechDocsTableOfContents(): Chainable<Element>;
/**
* Get the TechDocs navigation element
* @example cy.getTechDocsNavigation
*/
getTechDocsNavigation(): Chainable<Element>;
/**
* Intercept the TechDocs API calls
* @example cy.interceptTechDocsAPICalls
*/
interceptTechDocsAPICalls(): Chainable<Element>;
/**
* Mock SockJS-Node call
* @example cy.mockSockJSNode
*/
mockSockJSNode(): Chainable<Element>;
/**
* Wait TechDocs API response for home page
* @example cy.waitHomePage
*/
waitHomePage(): Chainable<Element>;
/**
* Wait TechDocs API response for Section 2 page
* @example cy.waitSectionTwoPage
*/
waitSectionTwoPage(): Chainable<Element>;
/**
* Check if the element is in viewport
* @example cy.isInViewport
*/
isInViewport(element: string): Chainable<Element>;
/**
* Check if the element is not in viewport
* @example cy.isNotInViewport
*/
isNotInViewport(element: string): Chainable<Element>;
}
}
@@ -57,7 +57,11 @@ export function ErrorPage(props: IErrorPageProps) {
<Grid container spacing={0} className={classes.container}>
<MicDrop />
<Grid item xs={12} sm={8} md={4}>
<Typography variant="body1" className={classes.subtitle}>
<Typography
data-testid="error"
variant="body1"
className={classes.subtitle}
>
ERROR {status}: {statusMessage}
</Typography>
<Typography variant="body1" className={classes.subtitle}>
@@ -88,6 +88,7 @@ export function HeaderTabs(props: HeaderTabsProps) {
{tabs.map((tab, index) => (
<TabUI
{...tab.tabProps}
data-testid={`header-tab-${index}`}
label={tab.label}
key={tab.id}
value={index}
@@ -62,6 +62,7 @@ export const DocsCardGrid = ({
name: toLowerMaybe(entity.metadata.name),
})}
color="primary"
data-testid="read_docs"
>
Read Docs
</Button>
@@ -108,7 +108,7 @@ const CustomPanel = ({
) : null}
</ContentHeader>
<div className={classes.panelContainer}>
<Panel entities={shownEntities} />
<Panel data-testid="techdocs-custom-panel" entities={shownEntities} />
</div>
</>
);
@@ -182,7 +182,7 @@ export const TechDocsCustomHome = ({
label,
}))}
/>
<Content>
<Content data-testid="techdocs-content">
{currentTabConfig.panels.map((config, index) => (
<CustomPanel
key={index}
@@ -19,6 +19,7 @@ import { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { techdocsApiRef } from '../../api';
import { Reader } from './Reader';
import { TechDocsNotFound } from './TechDocsNotFound';
import { TechDocsPageHeader } from './TechDocsPageHeader';
import { Content, Page } from '@backstage/core-components';
@@ -38,14 +39,19 @@ export const TechDocsPage = () => {
return Promise.resolve(undefined);
}, [kind, namespace, name, techdocsApi, documentReady]);
const { value: entityMetadataValue } = useAsync(() => {
return techdocsApi.getEntityMetadata({ kind, namespace, name });
}, [kind, namespace, name, techdocsApi]);
const { value: entityMetadataValue, error: entityMetadataError } =
useAsync(() => {
return techdocsApi.getEntityMetadata({ kind, namespace, name });
}, [kind, namespace, name, techdocsApi]);
const onReady = useCallback(() => {
setDocumentReady(true);
}, [setDocumentReady]);
if (entityMetadataError) {
return <TechDocsNotFound errorMessage={entityMetadataError.message} />;
}
return (
<Page themeId="documentation">
<TechDocsPageHeader
@@ -39,11 +39,15 @@ export const UserSettingsMenu = () => {
return (
<>
<IconButton aria-label="more" onClick={handleOpen}>
<IconButton
data-testid="user-settings-menu"
aria-label="more"
onClick={handleOpen}
>
<MoreVertIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
<MenuItem onClick={() => identityApi.signOut()}>
<MenuItem data-testid="sign-out" onClick={() => identityApi.signOut()}>
<ListItemIcon>
<SignOutIcon />
</ListItemIcon>
+1
View File
@@ -42,6 +42,7 @@ const getFilesToLint = () => {
// Note this ignore list only applies locally, CI runs `.github/workflows/docs-quality-checker.yml`
const ignored = ['', 'ADOPTERS.md', 'OWNERS.md'];
return execSync(command, {
stdio: ['ignore', 'pipe', 'inherit'],
})