Skip to content

Commit

Permalink
add playwright
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenkilbourn committed Aug 29, 2024
1 parent 4239d6d commit afe502e
Show file tree
Hide file tree
Showing 28 changed files with 883 additions and 8 deletions.
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@
"parserOptions": {
"project": ["./tsconfig.json"]
}
},
{
"files": "test/playwright/**",
"extends": "plugin:playwright/recommended"
}
]
}
35 changes: 35 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,38 @@ jobs:

- name: Test
run: yarn test

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: npx playwright install --with-deps
- name: Run Playwright tests
run: 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 @@ -76,3 +76,15 @@ dist
.vscode/*
!.vscode/settings.json.sample
lib

################################################
# Playwright
#
# Files generated by Playwright tests
################################################

**/playwrightTestData.json
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"lint:scripts": "eslint app/scripts/",
"lint:css": "stylelint 'app/styles/**/**' 'app/scripts/**/*.(js|ts|tsx|jsx)'",
"ts-check": "yarn tsc --noEmit --skipLibCheck",
"test": "jest"
"test": "jest",
"pretest:e2e": "node test/playwright/generateTestData.js",
"test:e2e": "yarn playwright test"
},
"targets": {
"veda-app": {
Expand Down Expand Up @@ -54,10 +56,12 @@
"@parcel/transformer-sass": "2.7.0",
"@parcel/transformer-typescript-types": "2.12.0",
"@parcel/transformer-webmanifest": "2.7.0",
"@playwright/test": "^1.46.1",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.2",
"@types/d3": "^7.4.0",
"@types/mapbox-gl": "^2.7.5",
"@types/node": "^22.5.0",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.12.0",
"babel-jest": "^28.1.3",
Expand All @@ -72,12 +76,13 @@
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-inclusive-language": "^2.1.1",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-playwright": "^1.6.2",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"events": "^3.3.0",
"fancy-log": "^1.3.3",
"fast-glob": "^3.2.7",
"fast-glob": "^3.3.2",
"fs-extra": "^10.0.0",
"gray-matter": "^4.0.3",
"gulp": "^4.0.2",
Expand Down
47 changes: 47 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { defineConfig, devices } from '@playwright/test';

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './test',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
timeout: 300000,
// For expect calls
expect: {
timeout: 180000,
},
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:9000',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'retain-on-failure',
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],

/* Run your local dev server before starting the tests */
webServer: {
timeout: 6 * 60 * 1000,
command: 'yarn serve',
url: 'http://localhost:9000',
reuseExistingServer: !process.env.CI,
},
});
48 changes: 48 additions & 0 deletions test/playwright/generateTestData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* eslint-disable fp/no-mutating-methods */
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 datasetIdDisableExplore = [];
const storyNames = [];

for (const catalog of catalogPaths) {
const catalogData = matter.read(catalog).data;
catalogNames.push(catalogData['name']);
datasetIds.push(catalogData['id']);
if (catalogData['disableExplore'] == true) {
datasetIdDisableExplore.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,
disabledDatasets: datasetIdDisableExplore
};

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 test/playwright/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 aboutParagraph: Locator;

constructor(page: Page) {
this.page = page;
this.aboutParagraph = this.page.getByText("The VEDA Dashboard is one of several user interfaces in the VEDA project");
}
}
58 changes: 58 additions & 0 deletions test/playwright/pages/basePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { test as base } from '@playwright/test';
import AboutPage from './aboutPage';
import CatalogPage from './catalogPage';
import ContactModal from './contactModal';
import DatasetPage from './datasetPage';
import DatasetSelectorComponent from './datasetSelectorComponent';
import ExplorePage from './explorePage';
import FooterComponent from './footerComponent';
import HeaderComponent from './headerComponent';
import NotebookConnectModal from './notebookConnectModal';
import StoriesPage from './storiesPage';


export const test = base.extend<{
aboutPage: AboutPage;
catalogPage: CatalogPage;
contactModal: ContactModal;
datasetSelectorComponent: DatasetSelectorComponent;
datasetPage: DatasetPage;
explorePage: ExplorePage;
footerComponent: FooterComponent;
headerComponent: HeaderComponent;
notebookConnectModal: NotebookConnectModal;
storiesPage: StoriesPage;
}> ({
aboutPage: async ({page}, use) => {
await use(new AboutPage(page));
},
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));
},
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));
},
notebookConnectModal: async ({page}, use) => {
await use(new NotebookConnectModal(page));
},
storiesPage: async ({page}, use) => {
await use(new StoriesPage(page));
},
});

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

export default class CatalogPage {
readonly page: Page;
readonly mainContent: Locator;
readonly header: Locator;
readonly accessDataButton: Locator;
readonly exploreDataButton: 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});
this.accessDataButton = this.page.getByRole('button', {name: /access data/i });
this.exploreDataButton = this.page.getByRole('button', {name: /explore data/i });
}

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();
// eslint-disable-next-line playwright/no-force-option
await catalogCard.click({force: true});
});
}
}
11 changes: 11 additions & 0 deletions test/playwright/pages/contactModal.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
26 changes: 26 additions & 0 deletions test/playwright/pages/datasetPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Locator, Page, test } from '@playwright/test';

export default class DatasetPage {
readonly page: Page;
readonly mainContent: Locator;
readonly header: Locator;
readonly exploreDataButton: Locator;
readonly analyzeDataButton: Locator;
readonly taxonomyLinkSection: 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} );
this.taxonomyLinkSection = this.page.locator('section').filter({has: page.getByRole('heading', {name: /taxonomy/i , includeHidden: true})});
}

async getAllTaxonomyLinks() {
return await test.step('get all links in taxonomy section', async() => {
return this.taxonomyLinkSection.locator('dd').getByRole('link').all();
});
}
}
24 changes: 24 additions & 0 deletions test/playwright/pages/datasetSelectorComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Locator, Page, test } from '@playwright/test';

export default class DatasetSelectorComponent {
readonly page: Page;
readonly article: Locator;
readonly addToMapButton: Locator;
readonly header: Locator;
readonly noDatasetMessage: Locator;

constructor(page: Page) {
this.page = page;
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() {
await test.step('add first dataset to map', async() => {
await this.article.first().click();
await this.addToMapButton.click();
});
}
}
25 changes: 25 additions & 0 deletions test/playwright/pages/explorePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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');
}

async closeFeatureTour() {
await test.step('close feature tour', async() => {
await this.closeFeatureTourButton.click();
});
}
}
Loading

0 comments on commit afe502e

Please sign in to comment.