diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index bc59f8ab6..c24289027 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -82,29 +82,38 @@ jobs: - name: Unit Test run: yarn test - # playwright_test: - # timeout-minutes: 60 - # runs-on: ubuntu-latest - # steps: - # - name: Checkout - # uses: actions/checkout@v3 - # with: - # submodules: recursive - # - name: Use Node.js ${{ env.NODE }} - # uses: actions/setup-node@v4 - # with: - # node-version: ${{ env.NODE }} - # - name: Install dependencies - # run: yarn - # - name: Install UI - # run: ./.veda/setup - # - name: Install Playwright Browsers - # run: npx playwright install --with-deps - # - name: Run Playwright tests - # run: MAPBOX_TOKEN="${{secrets.MAPBOX_TOKEN}}" yarn test:e2e - # - uses: actions/upload-artifact@v4 - # if: always() - # with: - # name: playwright-report - # path: playwright-report/ - # retention-days: 30 + playwright_test: + timeout-minutes: 60 + needs: prep + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Use Node.js ${{ env.NODE }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE }} + - name: Cache node_modules + uses: actions/cache@v2 + id: cache-node-modules + with: + path: | + node_modules + .veda/ui/node_modules + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} + - name: Install dependencies + run: yarn + - name: Install UI + run: ./.veda/setup + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: MAPBOX_TOKEN="${{secrets.MAPBOX_TOKEN}}" yarn test:e2e + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/e2e/generateTestData.js b/e2e/generateTestData.js index 1d2fb8cfd..369aba647 100644 --- a/e2e/generateTestData.js +++ b/e2e/generateTestData.js @@ -6,9 +6,12 @@ const fg = require('fast-glob'); const catalogPaths = fg.globSync('**/datasets/*.mdx'); const storyPaths = fg.globSync('**/stories/*.mdx'); const catalogNames = []; +const catalogNamesHidden = []; const datasetIds = []; +const datasetIdsHidden = []; const datasetIdDisableExplore = []; const storyNames = []; +const storyNamesHidden = []; for (const catalog of catalogPaths) { const catalogData = matter.read(catalog).data; @@ -17,18 +20,35 @@ for (const catalog of catalogPaths) { if(catalogData['disableExplore'] == true) { datasetIdDisableExplore.push(catalogData['id']) } + if(catalogData['isHidden'] == true) { + catalogNamesHidden.push(catalogData['name']) + datasetIdsHidden.push(catalogData['id']) + } } +const catalogNamesVisible = catalogNames.filter(item => !catalogNamesHidden.includes(item)); +const datasetIdsVisible = datasetIds.filter(item => !datasetIdsHidden.includes(item)); + for (const story of storyPaths) { const storyData = matter.read(story).data; - storyNames.push(storyData['name']); + storyNames.push(storyData['name']); + if(storyData['isHidden'] == true) { + storyNamesHidden.push(storyData['name']) + } } +const storyNamesVisible = storyNames.filter(item => !storyNamesHidden.includes(item)); const testDataJson = { catalogs: catalogNames, + catalogsHidden: catalogNamesHidden, + catalogsVisible: catalogNamesVisible, datasetIds: datasetIds, + datasetsIdsDisabled: datasetIdDisableExplore, + datasetIdsHidden: datasetIdsHidden, + datasetIdsVisible: datasetIdsVisible, stories: storyNames, - disabledDatasets: datasetIdDisableExplore, + storiesHidden: storyNamesHidden, + storiesVisible: storyNamesVisible, }; fs.writeFile( @@ -44,4 +64,4 @@ fs.writeFile( console.info('new test data file generated'); } } -); \ No newline at end of file +); diff --git a/e2e/pages/aboutPage.ts b/e2e/pages/aboutPage.ts index a9e84eac3..423cc115f 100644 --- a/e2e/pages/aboutPage.ts +++ b/e2e/pages/aboutPage.ts @@ -1,11 +1,33 @@ -import { Locator, Page } from '@playwright/test'; +import { Locator, Page, test } from '@playwright/test'; export default class AboutPage { readonly page: Page; - readonly mainContent: Locator; + readonly aboutParagraph: Locator; + readonly partnersSection: Locator; + readonly partnerLink: Locator; constructor(page: Page) { this.page = page; - this.mainContent = this.page.getByRole('main'); + this.aboutParagraph = this.page.getByText("The U.S. Greenhouse Gas Center (US GHG Center) is a multi-agency effort"); + this.partnersSection = this.page.locator('div').filter({has: this.page.getByRole('heading', { level: 2, name: /Our Partners/i })}); + this.partnerLink = this.partnersSection.getByRole('link').filter({has: this.page.locator('img')}); } -} \ No newline at end of file + + async getAllPartnerLinks() { + return await test.step('get all partner links', async() => { + return this.partnerLink.all(); + }) + } + + async getPartnerName(partner: Locator) { + console.log(`looking at ${partner}`) + const partnerText = await test.step('getting text name for partner', async() => { + // await partner.scrollIntoViewIfNeeded(); + const partnerText = await partner.innerText(); + console.log(`found ${partnerText}`) + return partnerText + }) + console.log(`found ${partnerText} here`) + return partnerText + } +} diff --git a/e2e/pages/analysisPage.ts b/e2e/pages/analysisPage.ts index b85189030..5c2379494 100644 --- a/e2e/pages/analysisPage.ts +++ b/e2e/pages/analysisPage.ts @@ -56,4 +56,4 @@ export default class AnalysisPage { this.datasetCheckbox.nth(index).click(); }) } -} \ No newline at end of file +} diff --git a/e2e/pages/analysisResultsPage.ts b/e2e/pages/analysisResultsPage.ts index 076b26ba8..b2fa81fc0 100644 --- a/e2e/pages/analysisResultsPage.ts +++ b/e2e/pages/analysisResultsPage.ts @@ -10,4 +10,4 @@ export default class AnalysisResultsPage { this.analysisCards = this.page.getByRole('article'); } -} \ No newline at end of file +} diff --git a/e2e/pages/basePage.ts b/e2e/pages/basePage.ts index 230a90ff7..792b6bb4d 100644 --- a/e2e/pages/basePage.ts +++ b/e2e/pages/basePage.ts @@ -3,28 +3,42 @@ import AboutPage from './aboutPage'; import AnalysisPage from './analysisPage'; import AnalysisResultsPage from './analysisResultsPage'; import CatalogPage from './catalogPage'; +import ContactModal from './contactModal'; import DatasetPage from './datasetPage'; import DatasetSelectorComponent from './datasetSelectorComponent'; +import DataToolkitPage from './dataToolkitPage'; import DisclaimerComponent from './disclaimerComponent'; import ExplorePage from './explorePage'; import FooterComponent from './footerComponent'; import HeaderComponent from './headerComponent'; import HomePage from './homePage'; +import LearnPage from './learnPage'; +import NewsPage from './newsPage'; +import NotebookConnectModal from './notebookConnectModal'; import StoryPage from './storyPage'; +import TopicsPage from './topicsPage'; +import SubscribePage from './subscribePage'; export const test = base.extend<{ aboutPage: AboutPage; analysisPage: AnalysisPage; analysisResultsPage: AnalysisResultsPage; catalogPage: CatalogPage; + contactModal: ContactModal; datasetPage: DatasetPage; datasetSelectorComponent: DatasetSelectorComponent; + dataToolkitPage: DataToolkitPage; disclaimerComponent: DisclaimerComponent; explorePage: ExplorePage; footerComponent: FooterComponent; headerComponent: HeaderComponent; homePage: HomePage; - storyPage: StoryPage + learnPage: LearnPage; + newsPage: NewsPage; + storyPage: StoryPage; + topicsPage: TopicsPage; + subscribePage: SubscribePage; + notebookConnectModal: NotebookConnectModal; }> ({ aboutPage: async ({page}, use) => { await use(new AboutPage(page)); @@ -38,12 +52,18 @@ export const test = base.extend<{ catalogPage: async ({page}, use) => { await use(new CatalogPage(page)); }, + contactModal: async ({page}, use) => { + await use(new ContactModal(page)); + }, datasetPage: async ({page}, use) => { await use(new DatasetPage(page)); }, datasetSelectorComponent: async ({page}, use) => { await use(new DatasetSelectorComponent(page)); }, + dataToolkitPage: async ({page}, use) => { + await use(new DataToolkitPage(page)); + }, disclaimerComponent: async ({page}, use) => { await use(new DisclaimerComponent(page)); }, @@ -59,9 +79,24 @@ export const test = base.extend<{ homePage: async ({page}, use) => { await use(new HomePage(page)); }, + learnPage: async ({page}, use) => { + await use(new LearnPage(page)); + }, + newsPage: async({page}, use) => { + await use(new NewsPage(page)) + }, + notebookConnectModal: async({page}, use) => { + await use(new NotebookConnectModal(page)) + }, storyPage: async ({page}, use) => { await use(new StoryPage(page)); }, + subscribePage: async ({page}, use) => { + await use(new SubscribePage(page)); + }, + topicsPage: async ({page}, use) => { + await use(new TopicsPage(page)); + }, }); -export const expect = test.expect; \ No newline at end of file +export const expect = test.expect; diff --git a/e2e/pages/catalogPage.ts b/e2e/pages/catalogPage.ts index 08a08bc3d..ae95e99c6 100644 --- a/e2e/pages/catalogPage.ts +++ b/e2e/pages/catalogPage.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test'; +import { Locator, Page, test } from '@playwright/test'; export default class CatalogPage { readonly page: Page; @@ -11,8 +11,16 @@ export default class CatalogPage { constructor(page: Page) { this.page = page; this.mainContent = this.page.getByRole('main'); - this.header = this.mainContent.getByRole('heading', {level: 1}) + this.header = this.mainContent.getByRole('heading', {level: 1, name: /data catalog/i}); this.accessDataButton = this.page.getByRole('button', {name: /access data/i }); this.exploreDataButton = this.page.getByRole('button', {name: /explore data/i }); } -} \ No newline at end of file + + async clickCatalogCard(item: string) { + await test.step(`click on catalog card for ${item}`, async() => { + const catalogCard = this.mainContent.getByRole('article').getByRole('heading', {level: 3, name: item, exact: true}).first(); + await catalogCard.scrollIntoViewIfNeeded(); + await catalogCard.click({force: true}); + }) + } +} diff --git a/e2e/pages/contactModal.ts b/e2e/pages/contactModal.ts new file mode 100644 index 000000000..e36e09daa --- /dev/null +++ b/e2e/pages/contactModal.ts @@ -0,0 +1,11 @@ +import { Locator, Page } from '@playwright/test'; + +export default class ContactModal { + readonly page: Page; + readonly header: Locator; + + constructor(page: Page) { + this.page = page; + this.header = this.page.getByRole("heading", {level: 1, name: /contact us/i }) + } +} diff --git a/e2e/pages/dataToolkitPage.ts b/e2e/pages/dataToolkitPage.ts new file mode 100644 index 000000000..086ca88ea --- /dev/null +++ b/e2e/pages/dataToolkitPage.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; + +export default class DataToolkitPage { + readonly page: Page; + readonly mainContent: Locator; + readonly header: Locator; + + + constructor(page: Page) { + this.page = page; + this.mainContent = this.page.getByRole('main'); + this.header = this.mainContent.getByRole('heading', {level: 1, name: /accessing and exploring data/i }) + } +} diff --git a/e2e/pages/datasetPage.ts b/e2e/pages/datasetPage.ts index 75386bca9..c71fe68ed 100644 --- a/e2e/pages/datasetPage.ts +++ b/e2e/pages/datasetPage.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test'; +import { Locator, Page, test } from '@playwright/test'; export default class DatasetPage { readonly page: Page; @@ -6,6 +6,7 @@ export default class DatasetPage { readonly header: Locator; readonly exploreDataButton: Locator; readonly analyzeDataButton: Locator; + readonly taxonomyLinkSection: Locator; constructor(page: Page) { @@ -14,5 +15,12 @@ export default class DatasetPage { this.header = this.mainContent.getByRole('heading', { level: 1 }) this.exploreDataButton = this.page.getByRole('link', {name: /explore data/i} ); this.analyzeDataButton = this.page.getByRole('button', {name: /analyze data/i} ); + this.taxonomyLinkSection = this.page.locator('section').filter({has: page.getByRole('heading', {name: /taxonomy/i , includeHidden: true})}); } -} \ No newline at end of file + + async getAllTaxonomyLinks() { + return await test.step('get all links in taxonomy section', async() => { + return this.taxonomyLinkSection.locator('dd').getByRole('link').all(); + }) + } +} diff --git a/e2e/pages/datasetSelectorComponent.ts b/e2e/pages/datasetSelectorComponent.ts index 3de9fa7e9..cc0f2ebd6 100644 --- a/e2e/pages/datasetSelectorComponent.ts +++ b/e2e/pages/datasetSelectorComponent.ts @@ -4,6 +4,7 @@ export default class DatasetSelectorComponent { readonly page: Page; readonly article: Locator; readonly addToMapButton: Locator; + readonly header: Locator; readonly noDatasetMessage: Locator; constructor(page: Page) { @@ -11,6 +12,7 @@ export default class DatasetSelectorComponent { this.article = this.page.getByRole('article'); this.addToMapButton = this.page.getByRole('button', {name: /add to map/i }); this.noDatasetMessage = this.page.getByText(/There are no datasets to show with the selected filters./i); + this.header = this.page.getByRole('heading', { level: 1, name: /data layers/i }); } async addFirstDataset() { @@ -19,4 +21,4 @@ export default class DatasetSelectorComponent { await this.addToMapButton.click(); }) } -} \ No newline at end of file +} diff --git a/e2e/pages/disclaimerComponent.ts b/e2e/pages/disclaimerComponent.ts index f743d4c5c..c7d19c49b 100644 --- a/e2e/pages/disclaimerComponent.ts +++ b/e2e/pages/disclaimerComponent.ts @@ -17,4 +17,4 @@ export default class DisclaimerComponent { await this.acceptButton.click(); }) } -} \ No newline at end of file +} diff --git a/e2e/pages/explorePage.ts b/e2e/pages/explorePage.ts index c06904403..7bc9b2225 100644 --- a/e2e/pages/explorePage.ts +++ b/e2e/pages/explorePage.ts @@ -1,13 +1,25 @@ -import { Locator, Page } from '@playwright/test'; +import { Locator, Page, test } from '@playwright/test'; export default class ExplorePage { readonly page: Page; readonly layersHeading: Locator; readonly mapboxCanvas: Locator; + readonly firstDatasetItem: Locator; + readonly closeFeatureTourButton: Locator; + readonly presetSelector: Locator; constructor(page: Page) { this.page = page; this.layersHeading = this.page.getByRole('heading', { name: 'Layers' }); this.mapboxCanvas = this.page.getByLabel('Map', { exact: true }); + this.firstDatasetItem = this.page.getByRole('article'); + this.closeFeatureTourButton = this.page.getByRole('button', { name: 'Close feature tour' }); + this.presetSelector = this.page.locator('#preset-selector'); } -} \ No newline at end of file + + async closeFeatureTour() { + await test.step('close feature tour', async() => { + await this.closeFeatureTourButton.click(); + }) + } +} diff --git a/e2e/pages/footerComponent.ts b/e2e/pages/footerComponent.ts index 90d5bdf8d..792af6b1e 100644 --- a/e2e/pages/footerComponent.ts +++ b/e2e/pages/footerComponent.ts @@ -1,13 +1,52 @@ -import { Locator, Page } from '@playwright/test'; +import { Locator, Page, test } from '@playwright/test'; + +type FooterLinkName = "stories" | "topics" | "dataToolkit" | "about" | "news" | "subscribe" export default class FooterComponent { readonly page: Page; readonly footer: Locator; - readonly partners: Locator; + readonly storiesLink: Locator; + readonly topicsLink: Locator; + readonly dataToolkitLink: Locator; + readonly aboutLink: Locator; + readonly newsLink: Locator; + readonly subscribeLink: Locator; constructor(page: Page) { this.page = page; this.footer = this.page.locator('footer'); - this.partners = this.footer.locator('div'); + this.storiesLink = this.footer.getByRole('link', { name: /stories/i} ); + this.topicsLink = this.footer.getByRole('link', { name: /topics/i} ); + this.dataToolkitLink = this.footer.getByRole('link', { name: /data toolkit/i} ); + this.aboutLink = this.footer.getByRole('link', { name: /about/i} ); + this.newsLink = this.footer.getByRole('link', { name: /news & events/i} ); + this.subscribeLink = this.footer.getByRole('link', { name: /subscribe/i} ); + } + + async clickLink(linkName: FooterLinkName) { + await test.step(`click on ${linkName} link`, async() => { + switch (linkName) { + case 'about': + await this.aboutLink.click(); + break; + case 'stories': + await this.storiesLink.click(); + break; + case 'topics': + await this.topicsLink.click(); + break; + case 'dataToolkit': + await this.dataToolkitLink.click(); + break; + case 'news': + await this.newsLink.click(); + break; + case 'subscribe': + await this.subscribeLink.click(); + break; + default: + throw new Error('unknown link referenced in footer test') + } + }) } -} \ No newline at end of file +} diff --git a/e2e/pages/headerComponent.ts b/e2e/pages/headerComponent.ts index 96808702a..6082297c2 100644 --- a/e2e/pages/headerComponent.ts +++ b/e2e/pages/headerComponent.ts @@ -1,23 +1,52 @@ -import { Locator, Page } from '@playwright/test'; +import { Locator, Page, test } from '@playwright/test'; + +type HeaderLinkName = "stories" | "topics" | "dataToolkit" | "news" | "about" | "contact" export default class HeaderComponent { readonly page: Page; - readonly header: Locator; - readonly welcomeLink: Locator; - readonly dataCatalogLink: Locator; - readonly analysisLink: Locator; - readonly dataInsightsLink: Locator; + readonly navigation: Locator; + readonly storiesLink: Locator; + readonly topicsLink: Locator; + readonly dataToolkitLink: Locator; + readonly newsLink: Locator; readonly aboutLink: Locator; - readonly feedbackLink: Locator; + readonly contactButton: Locator; constructor(page: Page) { this.page = page; - this.header = this.page.getByRole('navigation'); - this.welcomeLink = this.header.getByRole('link', {name: /welcome/i}); - this.dataCatalogLink = this.header.getByRole('link', {name: / data catalog/i}); - this.analysisLink = this.header.getByRole('link', {name: /analysis/i}); - this.dataInsightsLink = this.header.getByRole('link', {name: /data insights/i}); - this.aboutLink = this.header.getByRole('link', {name: /about/i}); - this.feedbackLink = this.header.getByRole('link', {name: /feedback/i}); + this.navigation = this.page.getByLabel('Global Navigation'); + this.storiesLink = this.navigation.getByRole('link', { name: /stories/i} ); + this.topicsLink = this.navigation.getByRole('link', { name: /topics/i} ); + this.dataToolkitLink = this.navigation.getByRole('link', { name: /data toolkit/i} ); + this.newsLink = this.navigation.getByRole('link', { name: /news & events/i }); + this.aboutLink = this.navigation.getByRole('link', { name: /about/i} ); + this.contactButton = this.navigation.getByRole('button', { name: /contact us/i} ); + } + + async clickLink(linkName: HeaderLinkName) { + await test.step(`click on ${linkName} link`, async() => { + switch (linkName) { + case 'about': + await this.aboutLink.click(); + break; + case 'stories': + await this.storiesLink.click(); + break; + case 'topics': + await this.topicsLink.click(); + break; + case 'dataToolkit': + await this.dataToolkitLink.click(); + break; + case 'news': + await this.newsLink.click(); + break; + case 'contact': + await this.contactButton.click(); + break; + default: + throw new Error('unknown link referenced in footer test') + } + }) } -} \ No newline at end of file +} diff --git a/e2e/pages/homePage.ts b/e2e/pages/homePage.ts index 5fa13895e..662515f3f 100644 --- a/e2e/pages/homePage.ts +++ b/e2e/pages/homePage.ts @@ -11,4 +11,4 @@ export default class HomePage { this.mainContent = this.page.getByRole('main'); this.headingContainer = this.mainContent.locator('div').filter({ hasText: 'U.S. Greenhouse Gas CenterUniting Data and Technology to Empower Tomorrow\'s' }).nth(2) } -} \ No newline at end of file +} diff --git a/e2e/pages/learnPage.ts b/e2e/pages/learnPage.ts new file mode 100644 index 000000000..1f6b281dd --- /dev/null +++ b/e2e/pages/learnPage.ts @@ -0,0 +1,11 @@ +import { Locator, Page, test, expect } from '@playwright/test'; + +export default class LearnPage { + readonly page: Page; + readonly header: Locator; + + constructor(page: Page) { + this.page = page; + this.header = this.page.getByRole('heading', { level: 1, name: 'Learn' }); + } +} diff --git a/e2e/pages/newsPage.ts b/e2e/pages/newsPage.ts new file mode 100644 index 000000000..ff5412e0a --- /dev/null +++ b/e2e/pages/newsPage.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; + +export default class NewsPage { + readonly page: Page; + readonly mainContent: Locator; + readonly header: Locator; + + + constructor(page: Page) { + this.page = page; + this.mainContent = this.page.getByRole('main'); + this.header = this.mainContent.getByRole('heading', {level: 1, name: /news & events/i }) + } +} diff --git a/e2e/pages/notebookConnectModal.ts b/e2e/pages/notebookConnectModal.ts new file mode 100644 index 000000000..bd8a9151c --- /dev/null +++ b/e2e/pages/notebookConnectModal.ts @@ -0,0 +1,11 @@ +import { Locator, Page } from '@playwright/test'; + +export default class NotebookConnectModal { + readonly page: Page; + readonly heading: Locator; + + constructor(page: Page) { + this.page = page; + this.heading = this.page.getByRole('heading', { name: /how to use this dataset/i }); + } +} diff --git a/e2e/pages/storyPage.ts b/e2e/pages/storyPage.ts index a087bfbc5..5264cffb4 100644 --- a/e2e/pages/storyPage.ts +++ b/e2e/pages/storyPage.ts @@ -9,6 +9,6 @@ export default class StoryPage { constructor(page: Page) { this.page = page; this.mainContent = this.page.getByRole('main'); - this.header = this.mainContent.getByRole('heading', {level: 1, name: /data insights/i }) + this.header = this.mainContent.getByRole('heading', {level: 1, name: /stories/i }) } -} \ No newline at end of file +} diff --git a/e2e/pages/subscribePage.ts b/e2e/pages/subscribePage.ts new file mode 100644 index 000000000..259b6629d --- /dev/null +++ b/e2e/pages/subscribePage.ts @@ -0,0 +1,11 @@ +import { Locator, Page } from '@playwright/test'; + +export default class SubscribePage { + readonly page: Page; + readonly header: Locator; + + constructor(page: Page) { + this.page = page; + this.header = this.page.getByText("Sign up for US GHG Center updates"); + } +} diff --git a/e2e/pages/topicsPage.ts b/e2e/pages/topicsPage.ts new file mode 100644 index 000000000..82c672598 --- /dev/null +++ b/e2e/pages/topicsPage.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; + +export default class TopicsPage { + readonly page: Page; + readonly mainContent: Locator; + readonly header: Locator; + + + constructor(page: Page) { + this.page = page; + this.mainContent = this.page.getByRole('main'); + this.header = this.mainContent.getByRole('heading', {level: 1, name: /topics/i }) + } +} diff --git a/e2e/tests/about.spec.ts b/e2e/tests/about.spec.ts new file mode 100644 index 000000000..3f902e771 --- /dev/null +++ b/e2e/tests/about.spec.ts @@ -0,0 +1,47 @@ +import { test, expect } from '../pages/basePage'; + +test('about page should have no javascript errors', async ({ + page, + aboutPage, + }) => { + let pageErrorCalled = false; + // Log all uncaught errors to the terminal + page.on('pageerror', exception => { + console.log(`Uncaught exception: "${exception}"`); + pageErrorCalled = true; + }); + + await page.goto('/about'); + await expect(aboutPage.aboutParagraph, `learn page should load`).toBeVisible(); + + // scroll page to bottom + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) +}); + +test('partner links should have url and image', async ({ + page, + aboutPage, + footerComponent + }) => { + + await page.goto('/about'); + await expect(aboutPage.aboutParagraph, `about page should load`).toBeVisible(); + await expect(footerComponent.footer).toBeVisible(); + + const partnerLinks = await aboutPage.getAllPartnerLinks(); + + for(const partner of partnerLinks) { + const partnerName = await test.step(' getting alt text', async() => { + return await partner.locator('img').getAttribute('alt'); + }) + + await test.step(`testing that ${partnerName} has an href and image`, async() => { + const href = await partner.getAttribute('href'); + const target = await partner.getAttribute('target'); + expect(href).not.toBeNull; + expect(target).toBe('_blank'); + await expect(partner.locator('img')).toBeVisible(); + }) + } +}); diff --git a/e2e/tests/catalog.spec.ts b/e2e/tests/catalog.spec.ts index c9e7c4b33..b9f7123d4 100644 --- a/e2e/tests/catalog.spec.ts +++ b/e2e/tests/catalog.spec.ts @@ -1,8 +1,9 @@ +import fs from 'fs'; import { test, expect } from '../pages/basePage'; -const catalogs = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogs']; +const visibleCatalogs = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogsVisible']; -test('load catalogs on /data-catalog route', async ({ +test('catalogs displayed on /data-catalog route', async ({ page, catalogPage, }) => { @@ -16,12 +17,15 @@ test('load catalogs on /data-catalog route', async ({ await page.goto('/data-catalog'); await expect(catalogPage.header, `catalog page should load`).toHaveText(/data catalog/i); - for (const item of catalogs) { - const catalogCard = catalogPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).last(); - await catalogCard.scrollIntoViewIfNeeded(); - await expect(catalogCard, `${item} catalog card should load`).toBeVisible(); + for (const item of visibleCatalogs) { + await test.step(`locate ${item} catalog card`, async() => { + const catalogCard = catalogPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).last(); + await catalogCard.scrollIntoViewIfNeeded(); + await expect(catalogCard, `${item} catalog card should load`).toBeVisible(); + }) + }; expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) -}); \ No newline at end of file +}); diff --git a/e2e/tests/catalogRouting.spec.ts b/e2e/tests/catalogRouting.spec.ts index 8161311d5..c0940b6d1 100644 --- a/e2e/tests/catalogRouting.spec.ts +++ b/e2e/tests/catalogRouting.spec.ts @@ -1,13 +1,15 @@ +import fs from 'fs'; import { test, expect } from '../pages/basePage'; -const catalogs = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogs']; +const visibleCatalogs = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogsVisible']; test.describe('catalog card routing', () => { - for (const item of catalogs) { - test(`${item} routes to dataset details page`, async({ + for (const item of visibleCatalogs) { + test(`${item} routes from catalog to details page`, async({ page, catalogPage, datasetPage, + notebookConnectModal, }) => { let pageErrorCalled = false; // Log all uncaught errors to the terminal @@ -17,19 +19,24 @@ test.describe('catalog card routing', () => { }); await page.goto('/data-catalog'); - await expect(catalogPage.header, `catalog page should load`).toHaveText(/data catalog/i); - - const catalogCard = catalogPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).first(); - await catalogCard.scrollIntoViewIfNeeded(); - await catalogCard.click({force: true}); + await expect(catalogPage.header, `catalog page should load`).toBeVisible(); + await catalogPage.clickCatalogCard(item); await expect(datasetPage.header.filter({ hasText: item}), `${item} page should load`).toBeVisible(); // scroll page to bottom - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await test.step('scroll to bottom of page', async() => { + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + }); expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) + + await test.step('click access data button', async() => { + await catalogPage.accessDataButton.click(); + }) + + await expect(notebookConnectModal.heading, 'modal should be visisble').toBeVisible(); }) } -}); \ No newline at end of file +}); diff --git a/e2e/tests/catalogTaxonomyLinks.spec.ts b/e2e/tests/catalogTaxonomyLinks.spec.ts new file mode 100644 index 000000000..8d6042d59 --- /dev/null +++ b/e2e/tests/catalogTaxonomyLinks.spec.ts @@ -0,0 +1,33 @@ +import fs from 'fs'; +import { test, expect } from '../pages/basePage'; + +const visibleCatalogs = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogsVisible']; + +test.describe('catalog card taxonomy pills have valid hyperlinks', () => { + for (const item of visibleCatalogs) { + test(`${item} details page has taxonomy hyperlinks`, async({ + page, + catalogPage, + datasetPage, + }) => { + + await page.goto('/data-catalog'); + await expect(catalogPage.header, `catalog page should load`).toHaveText(/data catalog/i); + + await catalogPage.clickCatalogCard(item); + + await expect(datasetPage.header.filter({ hasText: item}), `${item} page should load`).toBeVisible(); + + const taxonomyLinks = await datasetPage.getAllTaxonomyLinks(); + for(const link of taxonomyLinks) { + const linkName = await test.step('get link text', async() => { + return await link.innerText(); + }) + await test.step(`testing that ${linkName} has an href`, async() => { + const href = await link.getAttribute('href'); + expect(href).not.toBeNull; + }) + } + }) + } +}); diff --git a/e2e/tests/exploration.spec.ts b/e2e/tests/exploration.spec.ts index 551627c06..e22159410 100644 --- a/e2e/tests/exploration.spec.ts +++ b/e2e/tests/exploration.spec.ts @@ -24,8 +24,8 @@ test('explore a dataset', async ({ const mapboxResponse = await mapboxResponsePromise; expect(mapboxResponse.ok(), 'mapbox request should be successful').toBeTruthy(); - await page.getByRole('button', { name: 'Close feature tour' }).click(); - await expect(page.locator('#preset-selector')).toBeVisible(); + await explorePage.closeFeatureTour(); + await expect(explorePage.presetSelector, 'preset selector should be visible').toBeVisible(); await expect(explorePage.mapboxCanvas, 'mapbox canvas should be visible').toBeVisible(); diff --git a/e2e/tests/exploreDatasets.spec.ts b/e2e/tests/exploreDatasets.spec.ts index 9e95b93d1..ab9375fab 100644 --- a/e2e/tests/exploreDatasets.spec.ts +++ b/e2e/tests/exploreDatasets.spec.ts @@ -1,16 +1,16 @@ import fs from 'fs'; import { test, expect } from '../pages/basePage'; -const datasetIds = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8')).datasetIds; -const disabledDatasets = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8')).disabledDatasets; +const visibleDatasetIds = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8')).datasetIdsVisible; +const disabledDatasets = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8')).datasetsIdsDisabled; test.describe('explore dataset', () => { - for (const dataset of datasetIds) { + for (const dataset of visibleDatasetIds) { test(`${dataset} explore page functions`, async({ page, datasetSelectorComponent, disclaimerComponent, - datasetPage, + explorePage, }) => { let pageErrorCalled = false; // Log all uncaught errors to the terminal to be visible in trace @@ -23,20 +23,28 @@ test.describe('explore dataset', () => { await disclaimerComponent.acceptDisclaimer(); if(disabledDatasets.includes(dataset)){ - await expect(datasetSelectorComponent.noDatasetMessage).toBeVisible(); + await expect(datasetSelectorComponent.noDatasetMessage, 'dataset set to disabled').toBeVisible(); } else { - const collectionsResponsePromise = page.waitForResponse(response => - response.url().includes('collect') && response.status() === 200 - ); + const collectionsResponsePromise = test.step('wait for collect api response', () => { + return page.waitForResponse(response => + response.url().includes('collect') && response.status() === 200 + ); + }) + + const datasetName = await datasetSelectorComponent.article.first().getByRole('heading', {level: 3}).innerText() await datasetSelectorComponent.addFirstDataset() const mosaicResponse = await collectionsResponsePromise; expect(mosaicResponse.ok(), 'mapbox request should be successful').toBeTruthy(); + await explorePage.closeFeatureTour(); + await expect(explorePage.firstDatasetItem.getByRole('heading', {name: datasetName}).first(), `article with name ${dataset} should be visible`).toBeVisible(); // scroll page to bottom - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await test.step('scroll to bottom of the page', async() => { + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + }) expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false); } }) } -}); \ No newline at end of file +}); diff --git a/e2e/tests/footerNavigation.spec.ts b/e2e/tests/footerNavigation.spec.ts new file mode 100644 index 000000000..74340f1bf --- /dev/null +++ b/e2e/tests/footerNavigation.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '../pages/basePage'; + +test.describe('ensure links in footer route to expected page', async () => { + test('stories link', async({page, footerComponent, storyPage}) => { + await page.goto('/'); + await expect(footerComponent.footer).toBeVisible() + await footerComponent.clickLink('stories'); + await expect(storyPage.header).toBeVisible(); + await expect(page).toHaveURL(/\/stories/i); + }) + + test('topics link', async({page, footerComponent, topicsPage}) => { + await page.goto('/'); + await expect(footerComponent.footer, 'footer should be visible').toBeVisible() + await footerComponent.clickLink('topics'); + await expect(topicsPage.header).toBeVisible(); + await expect(page).toHaveURL(/\/topics/i); + }) + + test('data toolkit link', async({page, footerComponent, dataToolkitPage}) => { + await page.goto('/'); + await expect(footerComponent.footer, 'footer should be visible').toBeVisible() + await footerComponent.clickLink('dataToolkit'); + await expect(dataToolkitPage.header).toBeVisible(); + await expect(page).toHaveURL(/\/data-toolkit/i); + }) + + test('about link', async({page, footerComponent, aboutPage}) => { + await page.goto('/'); + await expect(footerComponent.footer, 'footer should be visible').toBeVisible() + await footerComponent.clickLink('about'); + await expect(aboutPage.aboutParagraph, 'about paragraph should be visible').toBeVisible(); + await expect(page, 'should navigate to about route').toHaveURL(/\/about/i); + }) + + + test('news & events link', async({page, footerComponent, newsPage}) => { + await page.goto('/'); + await expect(footerComponent.footer, 'footer should be visible').toBeVisible() + await footerComponent.clickLink('news'); + await expect(newsPage.header, 'news page header should be visibl').toBeVisible(); + await expect(page, 'should route to /news-and-events').toHaveURL(/\/news-and-events/i); + }) + + test('subscribe link', async({page, footerComponent, subscribePage}) => { + await page.goto('/'); + await expect(footerComponent.footer, 'footer should be visible').toBeVisible() + await footerComponent.subscribeLink.click(); + await expect(subscribePage.header, 'subscribe header should be visible').toBeVisible(); + await expect(page, 'should navigate to the subscription page').toHaveURL(/\/public\/subscription\/index.html/i); + }) +}) diff --git a/e2e/tests/stories.spec.ts b/e2e/tests/stories.spec.ts index 72a855beb..2e22c7ad6 100644 --- a/e2e/tests/stories.spec.ts +++ b/e2e/tests/stories.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '../pages/basePage'; -const stories = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['stories']; +const visibleStories = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['storiesVisible']; test('load stories on /stories route', async ({ page, @@ -19,12 +19,15 @@ test('load stories on /stories route', async ({ await page.goto('/stories'); await expect(storyPage.header, `data stories page should load`).toBeVisible(); - for (const item of stories) { - const storiesCard = storyPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).first(); - await storiesCard.scrollIntoViewIfNeeded(); - await expect(storiesCard, `${item} story card should load`).toBeVisible(); + for (const item of visibleStories) { + + await test.step(`look for article with heading ${item}`, async() => { + const storiesCard = storyPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).first(); + await storiesCard.scrollIntoViewIfNeeded(); + await expect(storiesCard, `${item} story card should load`).toBeVisible(); + }) }; expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) -}); \ No newline at end of file +}); diff --git a/e2e/tests/storiesRouting.spec.ts b/e2e/tests/storiesRouting.spec.ts index 89480f2c5..e1d419919 100644 --- a/e2e/tests/storiesRouting.spec.ts +++ b/e2e/tests/storiesRouting.spec.ts @@ -1,15 +1,16 @@ import { test, expect } from '../pages/basePage'; -const stories = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['stories']; +const visibleStories = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['storiesVisible']; test.describe('stories card routing', () => { - for (const item of stories) { - test(`${item} routes to dataset details page`, async({ + for (const item of visibleStories) { + + test(`${item} routes from stories to details page`, async({ page, storyPage, datasetPage, }) => { - let pageErrorCalled = false; + let pageErrorCalled = false; // Log all uncaught errors to the terminal page.on('pageerror', exception => { console.log(`Uncaught exception: "${exception}"`); @@ -19,12 +20,14 @@ test.describe('stories card routing', () => { await page.goto('/stories'); await expect(storyPage.header, `stories page should load`).toBeVisible(); - const storyCard = storyPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).last(); - await storyCard.scrollIntoViewIfNeeded(); - await storyCard.click({force: true}); + await test.step(`click on ${item} article card`, async() => { + const storyCard = storyPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).last(); + await storyCard.scrollIntoViewIfNeeded(); + await storyCard.click({force: true}); + }) await expect(datasetPage.header.filter({ hasText: item}), `${item} page should load`).toBeVisible(); expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) }) } -}); \ No newline at end of file +}); diff --git a/e2e/tests/topNavigation.spec.ts b/e2e/tests/topNavigation.spec.ts new file mode 100644 index 000000000..c009b1440 --- /dev/null +++ b/e2e/tests/topNavigation.spec.ts @@ -0,0 +1,52 @@ +import exp from 'constants'; +import { test, expect } from '../pages/basePage'; + +test.describe('ensure links in top navigation route to expected page', async () => { + test('stories link', async({page, headerComponent, storyPage}) => { + await page.goto('/'); + await expect(headerComponent.navigation, 'header should load').toBeVisible() + await headerComponent.clickLink('stories'); + await expect(storyPage.header, 'story page header should load').toBeVisible(); + await expect(page, 'should route to /stories').toHaveURL(/\/stories/i); + }) + + test('topics link', async({page, headerComponent, topicsPage}) => { + await page.goto('/'); + await expect(headerComponent.navigation, 'header should load').toBeVisible() + await headerComponent.clickLink('topics'); + await expect(topicsPage.header, 'topics page header should load').toBeVisible(); + await expect(page, 'should route to /topics').toHaveURL(/\/topics/i); + }) + + test('data toolkit link', async({page, headerComponent, dataToolkitPage}) => { + await page.goto('/'); + await expect(headerComponent.navigation, 'header should load').toBeVisible() + await headerComponent.clickLink('dataToolkit'); + await expect(dataToolkitPage.header, 'data toolkit page header should load').toBeVisible(); + await expect(page, 'should route to /data-toolkit').toHaveURL(/\/data-toolkit/i); + }) + + test('news & events link', async({page, headerComponent, newsPage}) => { + await page.goto('/'); + await expect(headerComponent.navigation, 'header should load').toBeVisible() + await headerComponent.clickLink('news'); + await expect(newsPage.header, 'news & events page header should be visible').toBeVisible(); + await expect(page, 'should route to /news-and-events').toHaveURL(/\/news-and-events/i); + }) + + test('about link', async({page, headerComponent, aboutPage}) => { + await page.goto('/'); + await expect(headerComponent.navigation, 'header should load').toBeVisible() + await headerComponent.clickLink('about') + await expect(aboutPage.aboutParagraph, 'about paragraph should be visible').toBeVisible(); + await expect(page, 'should route to about page').toHaveURL(/\/about/i); + }) + + test('contact us button', async({page, headerComponent, contactModal}) => { + await page.goto('/'); + await expect(headerComponent.navigation, 'header should load').toBeVisible() + await headerComponent.clickLink('contact'); + await expect(contactModal.header, 'contact modal header should be visible').toBeVisible(); + await expect(page.locator('iframe'), 'an iframe should be visible').toBeVisible(); + }) +}) diff --git a/package.json b/package.json index 7b87eab6d..60a1f9461 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "devDependencies": { "@parcel/packager-raw-url": "2.7.0", "@parcel/transformer-webmanifest": "2.7.0", - "@playwright/test": "^1.41.1", + "@playwright/test": "^1.46.0", "@types/node": "^20.11.6", "buffer": "^5.5.0||^6.0.0", "dotenv": "^10.0.0", diff --git a/playwright.config.ts b/playwright.config.ts index f493bc55b..8e892f65c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -51,7 +51,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - timeout: 3 * 60 * 1000, + timeout: 6 * 60 * 1000, command: 'yarn serve', url: 'http://localhost:9000', reuseExistingServer: !process.env.CI, diff --git a/tsconfig.json b/tsconfig.json index 607e8ff5f..dfcd4e8a1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "strictNullChecks": true, "baseUrl": "./.veda/ui", "jsx": "react", - "lib": [ "es2015" ], + "lib": [ "es2015", "dom" ], "paths": { "$veda-ui-scripts/*": ["./app/scripts/*"], "$veda-ui/*": ["./node_modules/*"], diff --git a/yarn.lock b/yarn.lock index f56dc8e80..ac96bf97a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -341,12 +341,12 @@ chrome-trace-event "^1.0.2" nullthrows "^1.1.1" -"@playwright/test@^1.41.1": - version "1.41.1" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.41.1.tgz#6954139ed4a67999f1b17460aa3d184f4b334f18" - integrity sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw== +"@playwright/test@^1.46.0": + version "1.46.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.46.0.tgz#ccea6d22c40ee7fa567e4192fafbdf2a907e2714" + integrity sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w== dependencies: - playwright "1.41.1" + playwright "1.46.0" "@types/node@^20.11.6": version "20.11.6" @@ -1107,17 +1107,17 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -playwright-core@1.41.1: - version "1.41.1" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.41.1.tgz#9c152670010d9d6f970f34b68e3e935d3c487431" - integrity sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg== +playwright-core@1.46.0: + version "1.46.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.46.0.tgz#2336ac453a943abf0dc95a76c117f9d3ebd390eb" + integrity sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A== -playwright@1.41.1: - version "1.41.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.41.1.tgz#83325f34165840d019355c2a78a50f21ed9b9c85" - integrity sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ== +playwright@1.46.0: + version "1.46.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.46.0.tgz#c7ff490deae41fc1e814bf2cb62109dd9351164d" + integrity sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw== dependencies: - playwright-core "1.41.1" + playwright-core "1.46.0" optionalDependencies: fsevents "2.3.2"