Skip to content

Commit

Permalink
Revert "move tests into ui submodule"
Browse files Browse the repository at this point in the history
This reverts commit 258e7fc.
  • Loading branch information
stephenkilbourn committed Feb 29, 2024
1 parent 258e7fc commit 2f6b1a6
Show file tree
Hide file tree
Showing 20 changed files with 500 additions and 313 deletions.
19 changes: 16 additions & 3 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Playwright E2E Testing

The veda-config can be tested end-to-end to by leveraging a suite of Playwright tests located in the veda-ui git submodule. 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 of the config repo.
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

Expand All @@ -11,12 +11,25 @@ The test suite can be run via the `yarn test:e2e` script. There is a `prtest:e2e
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 recommended ones are defined in the [Playwright documentation](https://playwright.dev/docs/locators#quick-guide).
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. This report can be viewed with the script `yarn report` from the config repo.
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.
10 changes: 3 additions & 7 deletions e2e/generateTestData.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@ 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) {
Expand All @@ -22,12 +19,11 @@ for (const story of storyPaths) {
}

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

fs.writeFile(path.join(__dirname, '../.veda/ui/e2e/playwrightTestData.json'), JSON.stringify(testDataJson), err => {
fs.writeFile(path.join(__dirname, 'playwrightTestData.json'), JSON.stringify(testDataJson), err => {
if (err) {
console.error(err);
} else {
Expand Down
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');
}
}
55 changes: 55 additions & 0 deletions e2e/pages/analysisPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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;


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

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

}
52 changes: 52 additions & 0 deletions e2e/pages/basePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { test as base } from '@playwright/test';
import AboutPage from './aboutPage';
import AnalysisPage from './analysisPage';
import AnalysisResultsPage from './analysisResultsPage';
import HomePage from './homePage';
import FooterComponent from './footerComponent';
import HeaderComponent from './headerComponent';
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;
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));
},
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})
}
}
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} );
}
}
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})
}
}
44 changes: 44 additions & 0 deletions e2e/tests/analysis.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { test, expect } from '../pages/basePage';

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

const mapboxResponsePromise = page.waitForResponse(/api\.mapbox.com\/v4\/mapbox\.mapbox-streets-v8/i);
await page.goto('/analysis');
await expect(analysisPage.header, `analysis page should load`).toBeVisible();
const mapboxResponse = await mapboxResponsePromise;
expect(mapboxResponse.ok(), 'mapbox request should be successful').toBeTruthy();
await expect(analysisPage.mapboxCanvas, 'mapbox canvas should be visible').toBeVisible();

const box = await analysisPage.mapboxCanvas.boundingBox();

// using Non-null Assertion because we know the mapbox is visible, therefore box is not null
const firstCorner = [box!.width / 4, box!.height / 4];
const secondCorner = [box!.width / 3, box!.height / 4];
const thirdCorner = [box!.width / 4, box!.height / 3];

await analysisPage.mapboxCanvas.click();

await analysisPage.drawPolygon([firstCorner, secondCorner, thirdCorner])

await analysisPage.clickDatasetOption(1);

const searchResponsePromise = page.waitForResponse(/\/search/i);
await analysisPage.generateAnalysisButton.click({force: true });


const searchResponse = await searchResponsePromise;
expect(searchResponse.ok(), 'request to GET /search should be successful').toBeTruthy();

await expect(analysisResultsPage.analysisCards.first(), 'at least one analysis results is visible' ).toBeVisible();

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

});
Loading

0 comments on commit 2f6b1a6

Please sign in to comment.