Skip to content
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

add playwright #363

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
36 changes: 36 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,39 @@ jobs:

- name: Test
run: yarn test

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
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,15 @@ dist

Makefile
.hypothesis

################################################
# Playwright Test Outputs
#
# Files generated by running Playwright tests
################################################

/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/e2e/playwrightTestData.json
35 changes: 35 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Playwright E2E Testing

This testing suite is for End to End testing the VEDA website via the UI using Playwright. It works by serving a local version of the site using yarn serve and performing UI checks against that locally hosted site. The suite is designed to generate a `playwrightTestData.json` that contains the list of all Catalog and Story names. This is done by parsing the `name` field in the `*.mdx` files of the `/datasets` and `/stories` directories.

## Running the test suite

The test suite can be run via the `yarn test:e2e` script. There is a `prtest:e2e` script that will generate a new playwrightTestData.json before beginning the actual playwright test run. This allows for new stories or catalogs to be added without updating the test suite.

## Directory Structure

The end to end tests are organized in the `/e2e` directory. The tests have been written following a [Page Object Model](https://martinfowler.com/bliki/PageObject.html) pattern.
Supporting files within the repo are in the following structure:

```text
/e2e
│─── README.md
│─── playwright.config.ts - imports our global setup, defines preferred browsers, & number of retries
│─── generateTestData.js - parses mdx files and creates a json based on their metadata
└─── /pages
│ └─── basePage.ts - imports all seeded data and PageObjects into our `test` object.
│ │
│ └─── [PAGENAME]Page.ts - The page objects. UI elements interacted with on a page are defined once to keep tests DRY and minimize test changes if layout changes.
└─── tests - our actual tests
```

## Updating Tests

If the layout of a page changes, then the tests may no longer be able to interact with locators. These locators are defined in the Page Objects defined in `/e2e/pages`. The Playwright framework provides multiple ways to choose elements to interact with. The recomended ones are defined in the [Playwright documentation](https://playwright.dev/docs/locators#quick-guide).

Any new pages will need to have new page objects created and then imported into the `basePage.ts` file following th format of existing pages. This allows all tests to reference the page.

## Output

Playwright will generate an html report with test results. This report will show all tests that were run, and will allow a user to view the results of any failed tests.
42 changes: 42 additions & 0 deletions e2e/generateTestData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const fg = require('fast-glob');

const catalogPaths = fg.globSync('**/datasets/*.mdx');
const storyPaths = fg.globSync('**/stories/*.mdx');
const catalogNames = [];
const datasetIds = [];
const storyNames = [];

for (const catalog of catalogPaths) {
const catalogData = matter.read(catalog).data;
catalogNames.push(catalogData['name']);
datasetIds.push(catalogData['id']);
}

for (const story of storyPaths) {
const storyData = matter.read(story).data;
storyNames.push(storyData['name']);
}

const testDataJson = {
catalogs: catalogNames,
datasetIds: datasetIds,
stories: storyNames
};

fs.writeFile(
path.join(__dirname, 'playwrightTestData.json'),
JSON.stringify(testDataJson),
(err) => {
if (err) {
// eslint-disable-next-line no-console
console.error(err);
throw err;
} else {
// eslint-disable-next-line no-console
console.info('new test data file generated');
}
}
);
11 changes: 11 additions & 0 deletions e2e/pages/aboutPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Locator, Page } from '@playwright/test';

export default class AboutPage {
readonly page: Page;
readonly mainContent: Locator;

constructor(page: Page) {
this.page = page;
this.mainContent = this.page.getByRole('main');
}
}
47 changes: 47 additions & 0 deletions e2e/pages/basePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { test as base } from '@playwright/test';
import AboutPage from './aboutPage';
import CatalogPage from './catalogPage';
import DatasetPage from './datasetPage';
import ExplorePage from './explorePage';
import FooterComponent from './footerComponent';
import HeaderComponent from './headerComponent';
import HomePage from './homePage';
import StoryPage from './storyPage';

export const test = base.extend<{
aboutPage: AboutPage;
catalogPage: CatalogPage;
datasetPage: DatasetPage;
explorePage: ExplorePage;
footerComponent: FooterComponent;
headerComponent: HeaderComponent;
homePage: HomePage;
storyPage: StoryPage
}> ({
aboutPage: async ({page}, use) => {
await use(new AboutPage(page));
},
catalogPage: async ({page}, use) => {
await use(new CatalogPage(page));
},
datasetPage: async ({page}, use) => {
await use(new DatasetPage(page));
},
explorePage: async ({page}, use) => {
await use(new ExplorePage(page));
},
footerComponent: async ({page}, use) => {
await use(new FooterComponent(page));
},
headerComponent: async ({page}, use) => {
await use(new HeaderComponent(page));
},
homePage: async ({page}, use) => {
await use(new HomePage(page));
},
storyPage: async ({page}, use) => {
await use(new StoryPage(page));
},
});

export const expect = test.expect;
14 changes: 14 additions & 0 deletions e2e/pages/catalogPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Locator, Page } from '@playwright/test';

export default class CatalogPage {
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})
}
}
18 changes: 18 additions & 0 deletions e2e/pages/datasetPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Locator, Page } from '@playwright/test';

export default class DatasetPage {
readonly page: Page;
readonly mainContent: Locator;
readonly header: Locator;
readonly exploreDataButton: Locator;
readonly analyzeDataButton: Locator;


constructor(page: Page) {
this.page = page;
this.mainContent = this.page.getByRole('main');
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} );
}
}
11 changes: 11 additions & 0 deletions e2e/pages/explorePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Locator, Page } from '@playwright/test';

export default class ExplorePage {
readonly page: Page;
readonly exploreDataLink: Locator;

constructor(page: Page) {
this.page = page;
this.exploreDataLink = this.page.getByRole('link', { name: /Explore data/i })
}
}
13 changes: 13 additions & 0 deletions e2e/pages/footerComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Locator, Page } from '@playwright/test';

export default class FooterComponent {
readonly page: Page;
readonly footer: Locator;
readonly partners: Locator;

constructor(page: Page) {
this.page = page;
this.footer = this.page.locator('footer');
this.partners = this.footer.locator('div');
}
}
23 changes: 23 additions & 0 deletions e2e/pages/headerComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Locator, Page } from '@playwright/test';

export default class HeaderComponent {
readonly page: Page;
readonly header: Locator;
readonly welcomeLink: Locator;
readonly dataCatalogLink: Locator;
readonly analysisLink: Locator;
readonly dataInsightsLink: Locator;
readonly aboutLink: Locator;
readonly feedbackLink: 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});
}
}
14 changes: 14 additions & 0 deletions e2e/pages/homePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Locator, Page } from '@playwright/test';

export default class HomePage {
readonly page: Page;
readonly mainContent: Locator;
readonly headingContainer: Locator;


constructor(page: Page) {
this.page = page;
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)
}
}
14 changes: 14 additions & 0 deletions e2e/pages/storyPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Locator, Page } from '@playwright/test';

export default class StoryPage {
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})
}
}
27 changes: 27 additions & 0 deletions e2e/tests/catalog.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { test, expect } from '../pages/basePage';

const catalogs = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogs'];

test('load catalogs on /data-catalog route', async ({
page,
catalogPage,
}) => {
let pageErrorCalled = false;
// Log all uncaught errors to the terminal
page.on('pageerror', exception => {
console.log(`Uncaught exception: "${exception}"`);
pageErrorCalled = true;
});

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();
};

expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false)

});
35 changes: 35 additions & 0 deletions e2e/tests/catalogRouting.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test, expect } from '../pages/basePage';

const catalogs = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogs'];

test.describe('catalog card routing', () => {
for (const item of catalogs) {
test(`${item} routes to dataset details page`, async({
page,
catalogPage,
datasetPage,
}) => {
let pageErrorCalled = false;
// Log all uncaught errors to the terminal
page.on('pageerror', exception => {
console.log(`Uncaught exception: "${exception}"`);
pageErrorCalled = true;
});

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(datasetPage.header.filter({ hasText: item}), `${item} 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)
})
}

});
Loading
Loading