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 initial playwright suite #835

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"ecmaVersion": 2018, // Allows for the parsing of modern ECMAScript features
"sourceType": "module" // Allows for the use of imports
},
"ignorePatterns": ["/e2e"],
"rules": {
"prettier/prettier": [
2,
Expand Down Expand Up @@ -113,6 +114,28 @@
"parserOptions": {
"project": ["./tsconfig.json"]
}
},
{
"files": ["e2e/**/*.ts", "e2e/**/*.tsx"],
"extends": [
"eslint:recommended",
"plugin:playwright/playwright-test"
],
"rules": {
"prettier/prettier": [
2,
{
"semi": true,
"singleQuote": true,
"jsxSingleQuote": true,
"parser": "flow"
}
],
"import-order": 2,
"semi": [2, "always"],
"no-extra-semi": 2,
"semi-spacing": [2, { "before": false, "after": true }]
}
}
]
}
79 changes: 79 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Playwright Tests
on:
pull_request:
types:
- opened
- synchronize
- reopened
- ready_for_review

env:
NODE: 16

jobs:
prep:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest

steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ github.token }}

- name: Checkout
uses: actions/checkout@v3

- name: Use Node.js ${{ env.NODE }}
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE }}

- name: Cache node_modules
uses: actions/cache@v3
id: cache-node-modules
with:
path: node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}

- name: Install
run: yarn

playwright:
timeout-minutes: 60
needs: prep
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Use Node.js ${{ env.NODE }}
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE }}

- name: Cache node_modules
uses: actions/cache@v3
id: cache-node-modules
with:
path: node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}

- name: Install
run: yarn install

- name: Install Playwright Browsers
run: yarn playwright install --with-deps

- name: Playwright tests
env:
MAPBOX_TOKEN: ${{secrets.MAPBOX_TOKEN}}
run: yarn test:e2e
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,9 @@ nbproject
temp
tmp
.tmp
dist
dist
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/e2e/playwrightTestData.json
2 changes: 1 addition & 1 deletion app/scripts/components/analysis/define/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@
const onDatasetLayerChange = useCallback(
(e) => {
const id = e.target.id;
let newDatasetsLayers = [...(datasetsLayers || [])];

Check warning on line 295 in app/scripts/components/analysis/define/index.tsx

View workflow job for this annotation

GitHub Actions / lint

Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator
if (e.target.checked) {
const newDatasetLayer = allAvailableDatasetsLayers.find(
(l) => l.id === id
Expand Down Expand Up @@ -333,7 +333,7 @@
setAnalysisParam('datasetsLayers', cleanedDatasetsLayers);
// Only update when stac search gets updated to avoid triggering an infinite
// read/set state loop
}, [selectableDatasetLayers, setAnalysisParam]);

Check warning on line 336 in app/scripts/components/analysis/define/index.tsx

View workflow job for this annotation

GitHub Actions / lint

React Hook useEffect has a missing dependency: 'datasetsLayers'. Either include it or remove the dependency array

const notReady = !readyToLoadDatasets || !datasetsLayers?.length;

Expand Down Expand Up @@ -463,7 +463,7 @@
<FoldBody>
{!infoboxMessage ? (
<>
<Form>
<Form data-testid='datasetOptions'>
<CheckableGroup>
{selectableDatasetLayers.map((datasetLayer) => (
<FormCheckableCustom
Expand Down
2 changes: 1 addition & 1 deletion app/scripts/components/analysis/results/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export default function AnalysisResults() {
</AnalysisFoldHeader>
<FoldBody>
{!!requestStatus.length && availableDomain && brushRange && (
<ChartCardList>
<ChartCardList data-testid='analysisCards'>
{requestStatus.map((l) => (
<li key={l.id}>
<ChartCard
Expand Down
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. The veda-ui runs the tests using mdx files in the mocks direcotry. The config repos can run these same tests against the mdx files in their `/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
│─── generateMockTestData.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.
46 changes: 46 additions & 0 deletions e2e/generateMockTestData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const fg = require('fast-glob');

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

const linkTestStories = ['Internal Link Test', 'External Link Test'];

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;
if (!linkTestStories.includes(storyData['name'])) {
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');
}
}
59 changes: 59 additions & 0 deletions e2e/pages/analysisPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Locator, Page, test } from '@playwright/test';

export default class AnalysisPage {
readonly page: Page;
readonly mainContent: Locator;
readonly header: Locator;
readonly mapboxCanvas: Locator;
readonly generateAnalysisButton: Locator;
readonly datasetOptions: Locator;
readonly datasetCheckbox: Locator;
readonly moreOptionsButton: Locator;
readonly northAmericaOption: Locator;


constructor(page: Page) {
this.page = page;
this.mainContent = this.page.getByRole('main');
this.header = this.mainContent.getByRole('heading', {level: 1, name: /analysis/i });
this.mapboxCanvas = this.page.getByLabel('Map', { exact: true });
this.generateAnalysisButton = this.page.getByRole('link', { name: /Generate analysis/i });
this.datasetOptions = this.page.getByTestId('datasetOptions');
this.datasetCheckbox = this.datasetOptions.getByRole('checkbox');
this.moreOptionsButton = this.page.getByRole('button', {name: /more options/i });
this.northAmericaOption = this.page.getByRole('button', {name: /north america/i });
}

async drawPolygon (polygonCorners: number[][]) {
await test.step('draw polygon on mapbox canvas box', async () => {
if(polygonCorners.length < 3) {
throw new Error('polygon in drawPolygon must have >=3 corners')
}
// mutating corners array to have all but the final corner
const finalCorner = polygonCorners.pop()|| [];

// single click each remaining corner
for (const corner of polygonCorners) {
await this.mapboxCanvas.click({
position: {
x: corner[0],
y: corner[1]
}
});
}
// double click on final corner
await this.mapboxCanvas.dblclick({
position: {
x: finalCorner[0],
y: finalCorner[1]
}
});
})
}

async clickDatasetOption (index: number) {
test.step(`clicking dataset number ${index}`, async () => {
this.datasetCheckbox.nth(index).locator('..').click();
})
}
}
13 changes: 13 additions & 0 deletions e2e/pages/analysisResultsPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Locator, Page } from '@playwright/test';

export default class AnalysisResultsPage {
readonly page: Page;
readonly analysisCards: Locator;


constructor(page: Page) {
this.page = page;
this.analysisCards = this.page.getByTestId('analysisCards');
}

}
57 changes: 57 additions & 0 deletions e2e/pages/basePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { test as base } from '@playwright/test';
import AboutPage from './aboutPage';
import AnalysisPage from './analysisPage';
import AnalysisResultsPage from './analysisResultsPage';
import ExplorePage from './explorePage';
import FooterComponent from './footerComponent';
import HeaderComponent from './headerComponent';
import HomePage from './homePage';
import CatalogPage from './catalogPage';
import DatasetPage from './datasetPage';
import StoryPage from './storyPage';

export const test = base.extend<{
aboutPage: AboutPage;
analysisPage: AnalysisPage;
analysisResultsPage: AnalysisResultsPage;
footerComponent: FooterComponent;
explorePage: ExplorePage;
headerComponent: HeaderComponent;
homePage: HomePage;
catalogPage: CatalogPage;
datasetPage: DatasetPage;
storyPage: StoryPage
}> ({
aboutPage: async ({page}, use) => {
await use(new AboutPage(page));
},
analysisPage: async ({page}, use) => {
await use(new AnalysisPage(page));
},
analysisResultsPage: async ({page}, use) => {
await use(new AnalysisResultsPage(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));
},
homePage: async ({page}, use) => {
await use(new HomePage(page));
},
storyPage: async ({page}, use) => {
await use(new StoryPage(page));
},
headerComponent: async ({page}, use) => {
await use(new HeaderComponent(page));
},
footerComponent: async ({page}, use) => {
await use(new FooterComponent(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, name: /data catalog/i});
}
}
Loading
Loading