Skip to content

Commit

Permalink
add boilerplate for playwright e2e testing (#2519)
Browse files Browse the repository at this point in the history
* add playwrite e2e testing
  • Loading branch information
CollinBeczak authored Jan 25, 2025
1 parent e1c7d8c commit 7332f79
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ src/customLayers.json
# testing
/coverage
chimp.js
error.png

# production
/build
Expand All @@ -37,3 +38,8 @@ yarn-error.log*

#vscode
/.vscode

# playwright
/test-results/
/playwright-report/
playwright/.auth/
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
"lint": "eslint src/",
"format": "biome format --write",
"check": "biome ci && eslint src/",
"explore": "source-map-explorer --only-mapped --no-border-checks 'dist/**/*.js'"
"explore": "source-map-explorer --only-mapped --no-border-checks 'dist/**/*.js'",
"test:e2e:start": "NODE_ENV=development yarn run build && npx serve dist",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
},
"browserslist": [
">0.2%",
Expand Down Expand Up @@ -120,8 +124,10 @@
"@biomejs/biome": "1.9.4",
"@eslint/js": "^9.9.0",
"@openstreetmap/id-tagging-schema": "^3.0.0",
"@playwright/test": "^1.49.1",
"@testing-library/jest-dom": "^6.4.6",
"@testing-library/react": "^12.1.2",
"@types/node": "^22.10.5",
"@vitejs/plugin-react-swc": "^3.7.1",
"@vitest/coverage-v8": "^2.1.2",
"dotenv": "^16.4.5",
Expand Down
87 changes: 87 additions & 0 deletions playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import path from "path";
import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import { fileURLToPath } from "url";

// Replicate __dirname functionality in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
dotenv.config({ path: path.resolve(__dirname, ".env.local") });

// Simplified environment variable handling
const requiredEnvVars = {
REACT_APP_USERNAME: process.env.REACT_APP_USERNAME,
REACT_APP_PASSWORD: process.env.REACT_APP_PASSWORD,
REACT_APP_URL: process.env.REACT_APP_URL,
};

// Validate required environment variables
Object.entries(requiredEnvVars).forEach(([key, value]) => {
if (!value) {
throw new Error(
`Required environment variable ${key} is missing. Please add it to .env.local`
);
}
});

/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
testDir: "./playwright/tests",
headless: true, // Run in headless mode for faster execution
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
globalSetup: "./playwright/global-setup.js",

use: {
baseURL: process.env.REACT_APP_URL || "http://localhost:3000",
storageState: "./playwright/.auth/state.json",
trace: "on-first-retry",
navigationTimeout: 30000,
actionTimeout: 15000,
},

projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
{
name: "edge",
use: {
...devices["Desktop Edge"],
},
},
],

webServer: {
command: "yarn run test:e2e:start",
url: process.env.REACT_APP_URL || "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 30000,
env: requiredEnvVars,
},
});
44 changes: 44 additions & 0 deletions playwright/global-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { chromium } from "@playwright/test";
import fs from "fs";
import path from "path";

async function globalSetup() {
const storageState = "./playwright/.auth/state.json";
const storageDir = path.dirname(storageState);

if (!fs.existsSync(storageDir)) {
fs.mkdirSync(storageDir, { recursive: true });
}
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

try {
// Navigate and sign in
await page.goto(process.env.REACT_APP_URL || "http://localhost:3000");
await page.locator("a").filter({ hasText: "Sign in" }).click();

// Handle OSM login
await page.locator("#username").fill(process.env.REACT_APP_USERNAME);
await page.locator("#password").fill(process.env.REACT_APP_PASSWORD);
await page.locator('input[type="submit"][value="Log in"]').click();

// Handle OAuth if needed
try {
const authorizeButton = await page.waitForSelector(
'input[type="submit"][value="Authorize"]',
{ timeout: 5000 }
);
if (authorizeButton) {
await authorizeButton.click();
}
} catch (e) {}

await context.storageState({ path: storageState });
} finally {
await context.close();
await browser.close();
}
}

export default globalSetup;
26 changes: 26 additions & 0 deletions playwright/tests/loggedInNavigation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test, expect } from "@playwright/test";

test.describe("Logged in navigation", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
await page
.getByRole("banner")
.locator("a")
.filter({ hasText: "Sign in" })
.click();
await page
.getByRole("link", { name: "My Points" })
.waitFor({ state: "visible", timeout: 5000 });
});

test("should navigate to Find Challenges", async ({ page }) => {
await page
.getByRole("navigation")
.getByRole("link", { name: "Find Challenges" })
.click();
await expect(
page.getByRole("heading", { name: "Challenges" }).locator("span")
).toBeVisible();
});
});
55 changes: 55 additions & 0 deletions playwright/tests/loggedOutNavigation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { test, expect } from "@playwright/test";

test.describe("Logged out navigation", () => {
test.use({ storageState: { cookies: [], origins: [] } });

test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
});

test("should load find challenges page", async ({ page }) => {
await page
.getByRole("navigation")
.getByRole("link", { name: "Find Challenges" })
.click();
await expect(
page.getByRole("heading", { name: "Challenges" }).locator("span")
).toBeVisible();
await expect(
page.locator("a").filter({ hasText: "Sign in" })
).toBeVisible();
});

test("should load leaderboard page", async ({ page }) => {
await page
.getByRole("navigation")
.getByRole("link", { name: "Leaderboard" })
.click();
await page.waitForLoadState("networkidle");
});

test("should load learn page", async ({ page }) => {
await page
.getByRole("navigation")
.getByRole("link", { name: "Learn" })
.click();
await page.waitForLoadState("networkidle");
});

test("should load blog page", async ({ page }) => {
await page
.getByRole("navigation")
.getByRole("link", { name: "Blog" })
.click();
await page.waitForLoadState("networkidle");
});

test("should load donate page", async ({ page }) => {
await page
.getByRole("navigation")
.getByRole("link", { name: "Donate" })
.click();
await page.waitForLoadState("networkidle");
});
});
25 changes: 25 additions & 0 deletions playwright/tests/login.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { test } from "@playwright/test";

test.describe("Logged in navigation", () => {
test.use({ storageState: { cookies: [], origins: [] } });

test("should login and redirect to maproulette", async ({ page }) => {
await page.goto("/");
await page
.getByRole("banner")
.locator("a")
.filter({ hasText: "Sign in" })
.click();
await page
.getByLabel("Email Address or Username")
.fill(process.env.REACT_APP_USERNAME || "");
await page
.getByLabel("Password")
.fill(process.env.REACT_APP_PASSWORD || "");
await page.getByRole("button", { name: "Log in" }).click();
await page.waitForLoadState("networkidle");
await page
.getByRole("link", { name: "My Points" })
.waitFor({ state: "visible", timeout: 5000 });
});
});
14 changes: 14 additions & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,18 @@ export default defineConfig({
__GIT_SHA__: JSON.stringify(execSync('git rev-parse HEAD').toString()),
__GIT_TAG__: JSON.stringify(execSync('git describe --tags --exact-match 2>/dev/null || true').toString()),
},
test: {
exclude: ['**/playwright/**', '**/node_modules/**', '**/dist/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'**/playwright/**',
'**/node_modules/**',
'**/dist/**',
'**/*.test.*',
'**/*.spec.*'
]
}
},
});
38 changes: 38 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,13 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==

"@playwright/test@^1.49.1":
version "1.49.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.49.1.tgz#55fa360658b3187bfb6371e2f8a64f50ef80c827"
integrity sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==
dependencies:
playwright "1.49.1"

"@popperjs/core@^2.8.4":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
Expand Down Expand Up @@ -1506,6 +1513,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b"
integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==

"@types/node@^22.10.5":
version "22.10.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.5.tgz#95af89a3fb74a2bb41ef9927f206e6472026e48b"
integrity sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==
dependencies:
undici-types "~6.20.0"

"@types/normalize-package-data@^2.4.0":
version "2.4.4"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
Expand Down Expand Up @@ -3839,6 +3853,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==

fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==

fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
Expand Down Expand Up @@ -5922,6 +5941,20 @@ piwik-react-router@^0.12.1:
url-join "^1.1.0"
warning "^3.0.0"

playwright-core@1.49.1:
version "1.49.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.49.1.tgz#32c62f046e950f586ff9e35ed490a424f2248015"
integrity sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==

playwright@1.49.1:
version "1.49.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.49.1.tgz#830266dbca3008022afa7b4783565db9944ded7c"
integrity sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==
dependencies:
playwright-core "1.49.1"
optionalDependencies:
fsevents "2.3.2"

polylabel@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/polylabel/-/polylabel-1.1.0.tgz#9483e64fc7a12a49f43e07e7a06752214ed2a8e7"
Expand Down Expand Up @@ -8000,6 +8033,11 @@ undici-types@~5.26.4:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==

undici-types@~6.20.0:
version "6.20.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==

unherit@^1.0.4:
version "1.1.3"
resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
Expand Down

0 comments on commit 7332f79

Please sign in to comment.