diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 0000000000..bcd60b826b --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,47 @@ +name: Playwright Tests +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + test: + timeout-minutes: 10 + runs-on: ubuntu-latest + env: + AIRTABLE_PEOPLE_BASE_ID: appk2btw36qEO3vFo + AIRTABLE_RESEARCH_BASE_ID: appTv9J1zxqaNgBHi + AIRTABLE_EVENTS_BASE_ID: tbl6CURONRn8ML6le + AIRTABLE_POSTS_BASE_ID: appsY0VXF7pbv3mKR + AIRTABLE_MITH_BASE_ID: appMWsw8HKjjokBg2 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + - name: Install dependencies + run: npm ci + - name: Cache Playwright browsers + uses: actions/cache@v3 + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('**/package-lock.json') }} + - name: Install Playwright Browsers + run: npx playwright install --with-deps + # - name: Start Gatsby development server + # env: + # AIRTABLE_TOKEN: ${{ secrets.AIRTABLE_TOKEN }} + # run: | + # npm run develop & + # npx wait-on http://localhost:8000 --timeout 300000 + + - name: Run Playwright tests + env: + AIRTABLE_TOKEN: ${{ secrets.AIRTABLE_TOKEN }} + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 0a80bc0edc..d6561ed961 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -7,7 +7,11 @@ on: workflow_dispatch: jobs: + test: + uses: ./.github/workflows/run-tests.yml + build: + needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 7ea3861c36..7d1f355732 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,9 @@ releases # Tool versioning info .tool-versions + +# Playwright tests +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/README.md b/README.md index d7596eab92..2a174f032a 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ You may also build the site locally and `rsync` the built site if you have the r To build the site you will need to install a few things: -* [Node](https://nodejs.org): a JavaScript programming environment -* [Git](https://git-scm.com/): version control software +- [Node](https://nodejs.org): a JavaScript programming environment +- [Git](https://git-scm.com/): version control software Then you will need to get this repository: @@ -46,3 +46,11 @@ You will also need a personal token from Airtable. Once logged in, you can gener Now you are ready to start the development server: npm run develop + +## Test + +To run the project's tests: + +`npm run test` + +You may also be required to install Playwright and test browser binaries with `npx playwright install` diff --git a/package-lock.json b/package-lock.json index aa7f184cad..77649e77a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,8 @@ "request": "^2.88.2" }, "devDependencies": { + "@playwright/test": "^1.45.1", + "@types/node": "^20.14.10", "airtable": "^0.12.2", "chalk": "^5.3.0", "dayjs": "^1.11.11", @@ -3939,6 +3941,21 @@ "@parcel/core": "^2.8.3" } }, + "node_modules/@playwright/test": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", + "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", + "dev": true, + "dependencies": { + "playwright": "1.45.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", @@ -4298,9 +4315,12 @@ } }, "node_modules/@types/node": { - "version": "14.18.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/node-fetch": { "version": "2.6.11", @@ -4836,6 +4856,12 @@ "node": ">=8.0.0" } }, + "node_modules/airtable/node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", + "dev": true + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -14791,6 +14817,50 @@ "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==" }, + "node_modules/playwright": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", + "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "dev": true, + "dependencies": { + "playwright-core": "1.45.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", + "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -18486,6 +18556,11 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, "node_modules/unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", diff --git a/package.json b/package.json index 903a6e7de0..bf22942d9c 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build": "gatsby build", "build-pages": "BASEPATH=/mith-static gatsby build --prefix-paths", "develop": "gatsby develop", + "test": "playwright test", "clean": "gatsby clean", "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"" }, @@ -49,6 +50,8 @@ "request": "^2.88.2" }, "devDependencies": { + "@playwright/test": "^1.45.1", + "@types/node": "^20.14.10", "airtable": "^0.12.2", "chalk": "^5.3.0", "dayjs": "^1.11.11", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..1b7851d303 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,81 @@ +import { defineConfig, devices } from "@playwright/test" + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* 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, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [["list", { printSteps: true }]], + /* 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://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + timeout: 100000, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "npm run start", + port: 8000, + reuseExistingServer: !process.env.CI, + timeout: 300000, + }, +}) diff --git a/src/templates/post-index.js b/src/templates/post-index.js index 582e55582c..8ee3d3b24a 100644 --- a/src/templates/post-index.js +++ b/src/templates/post-index.js @@ -1,56 +1,57 @@ -import React from 'react' -import { graphql, Link } from 'gatsby' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import React from "react" +import { graphql, Link } from "gatsby" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import Layout from '../components/layout' -import Paginator from '../components/paginator' -import SEO from '../components/seo' +import Layout from "../components/layout" +import Paginator from "../components/paginator" +import SEO from "../components/seo" -import './post-index.css' +import "./post-index.css" -const PostIndex = ({data}) => { +const PostIndex = ({ data }) => { const posts = data.allAirtablePosts.nodes const pageCount = data.allAirtablePosts.pageInfo.pageCount return ( - +
-

- News   +
-

+

News

+
+ {posts.map(_post => { const post = _post.data - const slug = '/news/' + post.slug - const markdownFile = post.slug + '.md' + const slug = "/news/" + post.slug + const markdownFile = post.slug + ".md" // pick out the markdown file that has the same slug - const doc = data.allFile.nodes.find( - n => n.childMarkdownRemark.fileAbsolutePath.match(markdownFile) + const doc = data.allFile.nodes.find(n => + n.childMarkdownRemark.fileAbsolutePath.match(markdownFile), ) // if there is no doc then we're missing the markdown file for a blog - // post that is in airtable - - if (! doc) { + // post that is in airtable + + if (!doc) { console.warn(`missing markdown post for slug ${post.slug}`) // throw new Error(`missing markdown post for slug ${post.slug}`) - return
-

- {post.post_title} -

-
- by {post.author_name} - {' '}on -
-
- Currently unavailable. -
-
+ return ( +
+

+ {post.post_title} +

+
+ by {post.author_name} on{" "} + +
+
Currently unavailable.
+
+ ) } return ( @@ -59,12 +60,14 @@ const PostIndex = ({data}) => { {post.post_title}
- by {post.author_name} - {' '}on + by {post.author_name} on{" "} +
- {doc.excerpt} - continue reading + {doc.excerpt} + + continue reading +
) @@ -81,7 +84,7 @@ export const query = graphql` allAirtablePosts( limit: $limit skip: $skip - sort: {data: {post_date: DESC}} + sort: { data: { post_date: DESC } } ) { nodes { data { @@ -96,7 +99,7 @@ export const query = graphql` pageCount } } - allFile(filter: {sourceInstanceName: {eq: "news"}}) { + allFile(filter: { sourceInstanceName: { eq: "news" } }) { nodes { childMarkdownRemark { excerpt(pruneLength: 250) @@ -107,5 +110,5 @@ export const query = graphql` } } ` - + export default PostIndex diff --git a/tests/pages/dialogues-page.ts b/tests/pages/dialogues-page.ts new file mode 100644 index 0000000000..5e19ac9e76 --- /dev/null +++ b/tests/pages/dialogues-page.ts @@ -0,0 +1,15 @@ +import type { Page, Locator } from "@playwright/test" + +export class DialoguesPage { + readonly page: Page + readonly visibleTitle: Locator + + constructor(page: Page) { + this.page = page + this.visibleTitle = page.locator("h1") + } + + async goto() { + await this.page.goto("/digital-dialogues") + } +} diff --git a/tests/pages/index.ts b/tests/pages/index.ts new file mode 100644 index 0000000000..1a55664b1f --- /dev/null +++ b/tests/pages/index.ts @@ -0,0 +1,5 @@ +export { ResearchPage } from "./research-page" +export { PeoplePage } from "./people-page" +export { NewsPage } from "./news-page" +export { DialoguesPage } from "./dialogues-page" +export { ValuesPage } from "./values-page" diff --git a/tests/pages/news-page.ts b/tests/pages/news-page.ts new file mode 100644 index 0000000000..991cabf95e --- /dev/null +++ b/tests/pages/news-page.ts @@ -0,0 +1,15 @@ +import type { Page, Locator } from "@playwright/test" + +export class NewsPage { + readonly page: Page + readonly visibleTitle: Locator + + constructor(page: Page) { + this.page = page + this.visibleTitle = page.locator("h1") + } + + async goto() { + await this.page.goto("/news") + } +} diff --git a/tests/pages/people-page.ts b/tests/pages/people-page.ts new file mode 100644 index 0000000000..c3cbceafba --- /dev/null +++ b/tests/pages/people-page.ts @@ -0,0 +1,15 @@ +import type { Page, Locator } from "@playwright/test" + +export class PeoplePage { + readonly page: Page + readonly visibleTitle: Locator + + constructor(page: Page) { + this.page = page + this.visibleTitle = page.locator("h1") + } + + async goto() { + await this.page.goto("/people") + } +} diff --git a/tests/pages/research-page.ts b/tests/pages/research-page.ts new file mode 100644 index 0000000000..eaecf362b8 --- /dev/null +++ b/tests/pages/research-page.ts @@ -0,0 +1,15 @@ +import type { Page, Locator } from "@playwright/test" + +export class ResearchPage { + readonly page: Page + readonly visibleTitle: Locator + + constructor(page: Page) { + this.page = page + this.visibleTitle = page.locator("h1") + } + + async goto() { + await this.page.goto("/research") + } +} diff --git a/tests/pages/values-page.ts b/tests/pages/values-page.ts new file mode 100644 index 0000000000..b7e13f8459 --- /dev/null +++ b/tests/pages/values-page.ts @@ -0,0 +1,15 @@ +import type { Page, Locator } from "@playwright/test" + +export class ValuesPage { + readonly page: Page + readonly visibleTitle: Locator + + constructor(page: Page) { + this.page = page + this.visibleTitle = page.locator("h1") + } + + async goto() { + await this.page.goto("/values") + } +} diff --git a/tests/top-level-pages.spec.ts b/tests/top-level-pages.spec.ts new file mode 100644 index 0000000000..6b7fc7ea8e --- /dev/null +++ b/tests/top-level-pages.spec.ts @@ -0,0 +1,50 @@ +import { + ResearchPage, + NewsPage, + PeoplePage, + DialoguesPage, + ValuesPage, +} from "./pages" +import { expect, test } from "@playwright/test" + +test("Research page has correct title and h1", async ({ page }) => { + const researchPage = new ResearchPage(page) + await researchPage.goto() + await expect(researchPage.page).toHaveTitle("MITH Research | MITH") + await expect(researchPage.visibleTitle).toHaveText("Research") +}) + +test("People page has correct title and h1", async ({ page }) => { + const peoplePage = new PeoplePage(page) + await peoplePage.goto() + await expect(peoplePage.page).toHaveTitle("People | MITH") + // Playwright's `toBeVisible` method checks if the element is in the DOM, not if it is hidden by CSS, as here + // so we expect this assertion to pass + await expect(peoplePage.visibleTitle).toBeVisible() + + const classNames = await peoplePage.visibleTitle.evaluate( + node => node.className, + ) + expect(classNames).toContain("text-hidden") +}) + +test("News page has correct title and h1", async ({ page }) => { + const newsPage = new NewsPage(page) + await newsPage.goto() + await expect(newsPage.page).toHaveTitle("MITH News | MITH") + await expect(newsPage.visibleTitle).toHaveText("News") +}) + +test("Dialogues page has correct title and h1", async ({ page }) => { + const dialoguesPage = new DialoguesPage(page) + await dialoguesPage.goto() + await expect(dialoguesPage.page).toHaveTitle("MITH Digital Dialogues | MITH") + await expect(dialoguesPage.visibleTitle).toHaveText("Digital Dialogues") +}) + +test("Values page has correct title and h1", async ({ page }) => { + const valuesPage = new ValuesPage(page) + await valuesPage.goto() + await expect(valuesPage.page).toHaveTitle("Our Values | MITH") + await expect(valuesPage.visibleTitle).toHaveText("Our Values") +})