Skip to content

Commit

Permalink
chore: add playwright (#1139)
Browse files Browse the repository at this point in the history
**Related Ticket:** #1024
#1197

### Description of Changes
This adds Playwright tests of some key user flows. The tests are set to
run on the github action. On failure, an html report along with traces
containing screenshots and network calls will be attached to the
action's step.

The tests will run only on release branches (branches starting with
`release`).

### Notes & Questions About Changes

### Validation / Testing
tests can be run locally with `yarn test:e2e` and they can also be seen
in the github action.
  • Loading branch information
dzole0311 authored Oct 23, 2024
2 parents 57bae88 + e4da6a8 commit c38b67d
Show file tree
Hide file tree
Showing 31 changed files with 971 additions and 9 deletions.
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@
"parserOptions": {
"project": ["./tsconfig.json"]
}
},
{
"files": "test/playwright/**",
"extends": "plugin:playwright/recommended"
}
]
}
36 changes: 36 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,39 @@ jobs:

- name: Test
run: yarn test

playwright:
timeout-minutes: 60
needs: prep
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/heads/release')

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: 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 @@ -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/
1 change: 1 addition & 0 deletions app/scripts/components/common/card/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,4 @@ function CardComponent(props: CardComponentPropsType) {
export const Card = styled(CardComponent)`
/* Convert to styled-component: https://styled-components.com/docs/advanced#caveat */
`;

4 changes: 3 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,13 @@ module.exports = {
// unmockedModulePathPatterns: undefined,

// Indicates whether each individual test should be reported during the run
verbose: true
verbose: true,

// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],

// Whether to use watchman for file crawling
// watchman: true,

modulePathIgnorePatterns: ['/test/playwright/']
};
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: 45000,
// 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");
}
}
63 changes: 63 additions & 0 deletions test/playwright/pages/basePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { test as base } from '@playwright/test';
import AboutPage from './aboutPage';
import CatalogPage from './catalogPage';
import ContactModal from './contactModal';
import ConsentBanner from './consentBanner';
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;
consentBanner: ConsentBanner;
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));
},
consentBanner: async ({page}, use) => {
await use(new ConsentBanner(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});
});
}
}
14 changes: 14 additions & 0 deletions test/playwright/pages/consentBanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Locator, Page } from '@playwright/test';

export default class ConsentBanner {
readonly page: Page;
readonly aboutParagraph: Locator;
readonly consentBanner: Locator;
readonly acceptButton: Locator;

constructor(page: Page) {
this.page = page;
this.consentBanner = this.page.locator('#cookie-consent');
this.acceptButton = this.consentBanner.getByRole('button', {name: /accept cookies/i});
}
}
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();
});
}
}
Loading

0 comments on commit c38b67d

Please sign in to comment.