diff --git a/jest.config.js b/jest.config.js index e824b7991f7..d85eae67b67 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,11 +15,13 @@ module.exports = { setupFilesAfterEnv: ['/jest.setup.ts'], moduleNameMapper: { '\\.(css|less|scss)$': '/src/__mocks__/styleMock.js', + '@docsearch/css(.*)': '/src/__mocks__/styleMock.js', '@/components/(.*)': '/src/components/$1', '@/constants/(.*)': '/src/constants/$1', '@/utils/(.*)': '/src/utils/$1', '@/data/(.*)': '/src/data/$1', - '@/directory/(.*)': '/src/directory/$1' + '@/directory/(.*)': '/src/directory/$1', + '@/themes/(.*)': '/src/themes/$1' }, transformIgnorePatterns: ['node_modules/(?!variables/.*)'] }; diff --git a/src/components/Accordion/__tests__/Accordion.test.tsx b/src/components/Accordion/__tests__/Accordion.test.tsx index 3decf2377c5..1b8f6141e98 100644 --- a/src/components/Accordion/__tests__/Accordion.test.tsx +++ b/src/components/Accordion/__tests__/Accordion.test.tsx @@ -46,25 +46,39 @@ describe('Accordion', () => { expect(bodyText).toBeInTheDocument(); }); - it('should expand Accordion when heading is clicked', async () => { + it('should toggle Accordion when heading is clicked', async () => { render(component); const accordionHeading = screen.getByText('Accordion component example'); + const detailsEl = await screen.getByRole('group'); + const text = await screen.getByText(content); userEvent.click(accordionHeading); await waitFor(() => { - expect(screen.getByText(content)).toBeInTheDocument(); - expect(screen.getByText(content)).toBeVisible(); + expect(text).toBeVisible(); + expect(detailsEl).toHaveAttribute('open'); + }); + + userEvent.click(accordionHeading); + await waitFor(() => { + expect(text).not.toBeVisible(); + expect(detailsEl).not.toHaveAttribute('open'); }); }); it('should collapse Accordion when close button is clicked', async () => { render(component); - await screen.getByText(content); + const accordionHeading = screen.getByText('Accordion component example'); + userEvent.click(accordionHeading); + const detailsEl = await screen.getByRole('group'); + expect(detailsEl).toHaveAttribute('open'); + + const text = await screen.getByText(content); const closeButton = screen.getByRole('button'); userEvent.click(closeButton); await waitFor(() => { - expect(screen.getByText(content)).not.toBeVisible(); + expect(text).not.toBeVisible(); + expect(detailsEl).not.toHaveAttribute('open'); }); }); diff --git a/src/components/FeatureFlags/__tests__/FeatureFlags.test.tsx b/src/components/FeatureFlags/__tests__/FeatureFlags.test.tsx new file mode 100644 index 00000000000..ee43dca6b21 --- /dev/null +++ b/src/components/FeatureFlags/__tests__/FeatureFlags.test.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import FeatureFlags from '../index'; +import FeatureFlagSummary from '../FeatureFlagSummary'; + +describe('FeatureFlags', () => { + it('should render the FeatureFlags component', async () => { + render(); + const element = await screen.findByRole('heading', { name: 'appSync' }); + expect(element).toBeInTheDocument(); + }); + + it('should render the FeatureFlagSummary component', async () => { + const flag = { + description: + 'Changes the permission format to grant access to graphql operations instead of appsync control plane operations', + type: 'Feature', + valueType: 'Boolean', + versionAdded: '4.42.0', + deprecationDate: 'May 1st 2021', + values: [ + { + value: 'true', + description: 'Creates IAM policies to allow Query/Mutations', + defaultNewProject: true, + defaultExistingProject: false + }, + { + value: 'false', + description: + 'Uses previous policy format which allows control plane access to AppSync', + defaultNewProject: false, + defaultExistingProject: true + } + ] + }; + + const component = ( + + ); + render(component); + const element = await screen.findByRole('link', { + name: 'generateGraphQLPermissions' + }); + expect(element).toBeInTheDocument(); + }); + + it('should render the FeatureFlagSummary component without description if one doesn"t exist on the flag', async () => { + const flag = { + type: 'Feature', + valueType: 'Boolean', + versionAdded: '4.42.0', + deprecationDate: 'May 1st 2021', + values: [ + { + value: 'true', + description: 'Creates IAM policies to allow Query/Mutations', + defaultNewProject: true, + defaultExistingProject: false + }, + { + value: 'false', + description: + 'Uses previous policy format which allows control plane access to AppSync', + defaultNewProject: false, + defaultExistingProject: true + } + ] + }; + + const component = ( + + ); + render(component); + const element = await screen.findByRole('heading', { + name: 'generateGraphQLPermissions' + }); + expect(element.nextElementSibling?.tagName).not.toBe('P'); + }); +}); diff --git a/src/components/FeatureLists/__tests__/FeatureLists.test.tsx b/src/components/FeatureLists/__tests__/FeatureLists.test.tsx new file mode 100644 index 00000000000..d6b8ce77a53 --- /dev/null +++ b/src/components/FeatureLists/__tests__/FeatureLists.test.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { FeatureList, FeatureItem, PlatformFeatureList } from '../index'; + +const routerMock = { + __esModule: true, + useRouter: () => { + return { + query: { platform: 'react' }, + pathname: '/gen1/', + asPath: '/gen1/' + }; + } +}; + +jest.mock('next/router', () => routerMock); + +describe('FeatureLists', () => { + const featureListComponent = ( + + + Deploy apps in Next.js, Nuxt.js, Gatsby, React, Vue, Angular (and more) + by simply connecting your Git repository. + + + ); + + it('should render the FeatureList component', async () => { + render(featureListComponent); + + const heading = await screen.findByRole('heading', { name: 'Deploy' }); + + expect(heading).toBeInTheDocument(); + expect(heading.tagName).toBe('H2'); + }); + + it('should render the FeatureItem component', async () => { + render(featureListComponent); + + const link = await screen.findByRole('link', { + name: 'SSR/SSG/ISR hosting support' + }); + + expect(link).toBeInTheDocument(); + }); + + it('should render the PlatformFeatureList component', async () => { + render(); + + const link = await screen.findByRole('link', { + name: 'Simple configuration' + }); + + const heading = await screen.findByRole('heading', { + name: 'Features for React' + }); + + expect(link).toBeInTheDocument(); + expect(heading).toBeInTheDocument(); + }); +}); diff --git a/src/components/Feedback/__tests__/index.test.tsx b/src/components/Feedback/__tests__/index.test.tsx index f93e48ff9f4..7b02eb5d5a4 100644 --- a/src/components/Feedback/__tests__/index.test.tsx +++ b/src/components/Feedback/__tests__/index.test.tsx @@ -3,7 +3,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import * as trackModule from '../../../utils/track'; -jest.mock('next/router', () => ({ +const router = jest.mock('next/router', () => ({ useRouter() { return { route: '/', @@ -19,6 +19,8 @@ jest.mock('../../../utils/track', () => ({ })); describe('Feedback', () => { + const component = ; + it('should render component with thumbs up and thumbs down button', () => { const component = ; @@ -32,27 +34,22 @@ describe('Feedback', () => { }); it('should show response text when No is clicked', async () => { - const component = ; - render(component); - const thumbsDownButton = screen.getByText('No'); - const feedbackComponent = screen.getByText('Was this page helpful?'); - const feedbackText = screen.getByText('Can you provide more details?'); + const thumbsDownButton = screen.getByRole('button', { name: 'No' }); expect(thumbsDownButton).toBeInTheDocument(); - userEvent.click(feedbackComponent); + userEvent.click(thumbsDownButton); + const response = screen.getByRole('link'); await waitFor(() => { - expect(feedbackText).toBeVisible(); + expect(response.textContent).toBe('File an issue on GitHub'); }); }); it('should call trackFeedbackSubmission request when either button is clicked', async () => { jest.spyOn(trackModule, 'trackFeedbackSubmission'); - const component = ; - render(component); const thumbsDownButton = screen.getByText('No'); userEvent.click(thumbsDownButton); diff --git a/src/components/FrameworkGrid/__tests__/FrameworkGrid.test.tsx b/src/components/FrameworkGrid/__tests__/FrameworkGrid.test.tsx new file mode 100644 index 00000000000..30fb0e40bbc --- /dev/null +++ b/src/components/FrameworkGrid/__tests__/FrameworkGrid.test.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { FrameworkGrid } from '../index'; + +const routerMock = { + __esModule: true, + useRouter: () => { + return { + query: { platform: 'react' }, + pathname: '/gen1/', + asPath: '/gen1/' + }; + } +}; + +jest.mock('next/router', () => routerMock); + +describe('FrameworkGrid', () => { + const component = ; + + it('should render the FrameworkGrid component', async () => { + render(component); + const framework = await screen.findByRole('link', { name: 'React' }); + expect(framework).toBeInTheDocument(); + }); + + it('should highlight icon of the currentKey', async () => { + render(component); + const framework = await screen.findByRole('link', { name: 'React' }); + expect(framework.classList).toContain('framework-grid__link--current'); + }); +}); diff --git a/src/components/GetStartedPopover/__tests__/GetStartedPopover.test.tsx b/src/components/GetStartedPopover/__tests__/GetStartedPopover.test.tsx index 230ec106964..759e5eea6e0 100644 --- a/src/components/GetStartedPopover/__tests__/GetStartedPopover.test.tsx +++ b/src/components/GetStartedPopover/__tests__/GetStartedPopover.test.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { render, screen } from '@testing-library/react'; -import { GetStartedPopover } from '../index'; +import { GetStartedPopover, generateGetStartedLinks } from '../index'; import userEvent from '@testing-library/user-event'; import { IconAndroid, @@ -34,7 +34,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'react' } }, - icon: + icon: , + platform: 'react' }, { title: 'JavaScript', @@ -42,7 +43,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'javascript' } }, - icon: + icon: , + platform: 'javascript' }, { title: 'Flutter', @@ -50,7 +52,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'flutter' } }, - icon: + icon: , + platform: 'flutter' }, { title: 'Swift', @@ -58,7 +61,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'swift' } }, - icon: + icon: , + platform: 'swift' }, { title: 'Android', @@ -66,7 +70,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'android' } }, - icon: + icon: , + platform: 'android' }, { title: 'React Native', @@ -74,7 +79,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'react-native' } }, - icon: + icon: , + platform: 'react-native' }, { title: 'Angular', @@ -82,7 +88,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'angular' } }, - icon: + icon: , + platform: 'angular' }, { title: 'Next.js', @@ -90,7 +97,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'nextjs' } }, - icon: + icon: , + platform: 'nextjs' }, { title: 'Vue', @@ -98,7 +106,8 @@ describe('GetStartedPopover', () => { pathname: getStartedHref, query: { platform: 'vue' } }, - icon: + icon: , + platform: 'vue' } ]; @@ -106,6 +115,13 @@ describe('GetStartedPopover', () => { ); + const componentWithGeneratedLinks = ( + + ); + it('should render the GetStartedPopover component', async () => { render(component); @@ -285,4 +301,21 @@ describe('GetStartedPopover', () => { '/gen1/nextjs/start/getting-started/introduction' ); }); + + it('should generate getStartedLinks using generateGetStartedLinks function', async () => { + render(componentWithGeneratedLinks); + + const swiftOption = await screen.findByRole('link', { name: 'Swift' }); + const angularOption = await screen.findByRole('link', { name: 'Angular' }); + const nextjsOption = await screen.findByRole('link', { name: 'Next.js' }); + expect(swiftOption.getAttribute('href')).toContain( + '/swift/start/quickstart' + ); + expect(angularOption.getAttribute('href')).toContain( + '/angular/start/quickstart' + ); + expect(nextjsOption.getAttribute('href')).toContain( + '/nextjs/start/quickstart' + ); + }); }); diff --git a/src/components/GlobalNav/__tests__/GlobalNav.test.tsx b/src/components/GlobalNav/__tests__/GlobalNav.test.tsx new file mode 100644 index 00000000000..9de9a3f5ae7 --- /dev/null +++ b/src/components/GlobalNav/__tests__/GlobalNav.test.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { LEFT_NAV_LINKS, RIGHT_NAV_LINKS } from '@/utils/globalnav'; +import { GlobalNav, NavMenuItem } from '@/components/GlobalNav/GlobalNav'; + +const routerMock = { + __esModule: true, + useRouter: () => { + return { + query: { platform: 'react' }, + pathname: '/', + asPath: '/' + }; + } +}; + +jest.mock('next/router', () => routerMock); + +describe('GlobalNav', () => { + const component = ( + + ); + + it('should render the GlobalNav component', async () => { + render(component); + const link = await screen.findByRole('link', { name: 'About AWS Amplify' }); + expect(link).toBeInTheDocument(); + }); +}); diff --git a/src/components/Layout/LayoutHeader.tsx b/src/components/Layout/LayoutHeader.tsx index 67bc553a84f..42e09ea83a1 100644 --- a/src/components/Layout/LayoutHeader.tsx +++ b/src/components/Layout/LayoutHeader.tsx @@ -12,7 +12,7 @@ import { IconMenu, IconDoubleChevron } from '@/components/Icons'; import { Menu } from '@/components/Menu'; import { LayoutContext } from '@/components/Layout'; import { PlatformNavigator } from '@/components/PlatformNavigator'; -import flatDirectory from 'src/directory/flatDirectory.json'; +import flatDirectory from '@/directory/flatDirectory.json'; import { DocSearch } from '@docsearch/react'; import '@docsearch/css'; import { PageLastUpdated } from '../PageLastUpdated'; diff --git a/src/components/Layout/__tests__/Layout.test.tsx b/src/components/Layout/__tests__/Layout.test.tsx new file mode 100644 index 00000000000..ac116f9725a --- /dev/null +++ b/src/components/Layout/__tests__/Layout.test.tsx @@ -0,0 +1,111 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { Layout } from '../index'; +import userEvent from '@testing-library/user-event'; + +const routerMock = { + __esModule: true, + useRouter: () => { + return { + query: { platform: 'react' }, + pathname: '/[platform]/start/getting-started/introduction/', + asPath: '/[platform]/start/getting-started/introduction/' + }; + } +}; + +jest.mock('next/router', () => routerMock); + +describe('Layout', () => { + const layoutComponent = ( + + <> + + ); + + it('should render the Layout component', async () => { + render(layoutComponent); + const layout = await screen.getByRole('main', { name: 'Main content' }); + expect(layout).toBeInTheDocument(); + }); + + it('should show Layout with system color mode', async () => { + render(layoutComponent); + + const nav = await screen.getByRole('navigation', { + name: 'Amplify Docs - External links to additional Amplify resources' + }); + + const themeWrapper = nav.parentElement?.parentElement; + + expect(themeWrapper).toHaveAttribute('data-amplify-color-mode', 'system'); + }); + + it('should show Layout with dark color mode', async () => { + localStorage.setItem('colorMode', 'dark'); + render(layoutComponent); + + const nav = await screen.getByRole('navigation', { + name: 'Amplify Docs - External links to additional Amplify resources' + }); + + const themeWrapper = nav.parentElement?.parentElement; + + expect(themeWrapper).toHaveAttribute('data-amplify-color-mode', 'dark'); + }); + + it('should open menu on click of Menu button in mobile', async () => { + render(layoutComponent); + const menuButton = await screen.getByRole('button', { name: 'Menu' }); + const closeButton = await screen.getByRole('button', { + name: 'Close menu' + }); + expect(closeButton.classList).not.toContain( + 'layout-sidebar__mobile-toggle--open' + ); + userEvent.click(menuButton); + expect(closeButton.classList).toContain( + 'layout-sidebar__mobile-toggle--open' + ); + }); + + it('should close menu on click of Close button in mobile', async () => { + render(layoutComponent); + const menuButton = await screen.getByRole('button', { name: 'Menu' }); + const closeButton = await screen.getByRole('button', { + name: 'Close menu' + }); + userEvent.click(menuButton); + userEvent.click(closeButton); + expect(closeButton.classList).not.toContain( + 'layout-sidebar__mobile-toggle--open' + ); + expect(menuButton).toHaveFocus(); + }); + + it('should close menu on click outside of menu in mobile', async () => { + render(layoutComponent); + const menuButton = await screen.getByRole('button', { name: 'Menu' }); + const closeButton = await screen.getByRole('button', { + name: 'Close menu' + }); + const outsideMenu = document.getElementsByClassName( + 'layout-sidebar__backdrop--expanded' + ); + userEvent.click(menuButton); + userEvent.click(outsideMenu[0]); + + expect(closeButton.classList).not.toContain( + 'layout-sidebar__mobile-toggle--open' + ); + }); +}); diff --git a/src/components/SkipToMain/__tests__/SkipToMain.test.tsx b/src/components/SkipToMain/__tests__/SkipToMain.test.tsx new file mode 100644 index 00000000000..33e8e1e264b --- /dev/null +++ b/src/components/SkipToMain/__tests__/SkipToMain.test.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { render, screen } from '@testing-library/react'; +import { SkipToMain } from '../index'; + +describe('SkipToMain', () => { + const component = ; + + it('should render the SkipToMain component', async () => { + render(component); + const link = await screen.findByRole('link', { + name: 'Skip to main content' + }); + expect(link).toBeInTheDocument(); + expect(link.getAttribute('href')).toBe('#pageMain'); + }); +});