Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Security Notes
# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions)
# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions.
# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!!
# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags.
# MERGE QUEUE NOTE: This Workflow does not run on `merge_group` trigger, as this Workflow is not required for Merge Queue's

name: Playwright Tests

on:
pull_request:
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
actions: read

jobs:
get-vercel-preview:
name: Get Vercel Preview
runs-on: ubuntu-latest
outputs:
deployment_found: ${{ steps.set_outputs.outputs.deployment_found }}
url: ${{ steps.set_outputs.outputs.url }}
steps:
- name: Capture Vercel Preview
id: check_deployment
uses: patrickedqvist/wait-for-vercel-preview@06c79330064b0e6ef7a2574603b62d3c98789125 # v1.3.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 300 # timeout after 5 minutes
check_interval: 10 # check every 10 seconds
continue-on-error: true
- name: Set Outputs
if: always()
id: set_outputs
run: |
if [[ -z "${{ steps.check_deployment.outputs.url }}" ]]; then
echo "deployment_found=false" >> $GITHUB_OUTPUT
else
echo "deployment_found=true" >> $GITHUB_OUTPUT
echo "url=${{ steps.check_deployment.outputs.url }}" >> $GITHUB_OUTPUT
fi

playwright:
needs: get-vercel-preview
if: needs.get-vercel-preview.outputs.deployment_found == 'true'
name: Playwright Tests
runs-on: ubuntu-latest

steps:
- name: Harden Runner
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit

- name: Git Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Set up pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
cache: true

- name: Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
# We want to ensure that the Node.js version running here respects our supported versions
node-version-file: '.nvmrc'
cache: 'pnpm'

- name: Install packages
run: pnpm install --frozen-lockfile

- name: Get Playwright version
id: playwright-version
working-directory: apps/site
run: echo "version=$(pnpm exec playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT

- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}

- name: Install Playwright Browsers
working-directory: apps/site
run: pnpm exec playwright install --with-deps

- name: Run Playwright tests
working-directory: apps/site
run: pnpm playwright
env:
VERCEL_PREVIEW_URL: ${{ needs.get-vercel-preview.outputs.url }}

- name: Upload Playwright test results
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-report
path: apps/site/playwright-report/
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ cache

# TypeScript
tsconfig.tsbuildinfo

dist/

# Ignore the blog-data json that we generate during dev and build
Expand All @@ -43,3 +42,7 @@ apps/site/public/blog-data.json
apps/site/.open-next
apps/site/.wrangler


## Playwright
test-results
playwright-report
18 changes: 18 additions & 0 deletions COLLABORATOR_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [Adding a Download Package Manager](#adding-a-download-package-manager)
- [Unit Tests and Storybooks](#unit-tests-and-storybooks)
- [General Guidelines for Unit Tests](#general-guidelines-for-unit-tests)
- [General Guidelines for Playwright E2E Tests](#general-guidelines-for-playwright-e2e-tests)
- [General Guidelines for Storybooks](#general-guidelines-for-storybooks)
- [Remarks on Technologies used](#remarks-on-technologies-used)
- [Seeking additional clarification](#seeking-additional-clarification)
Expand Down Expand Up @@ -437,6 +438,23 @@ Unit Tests are fundamental to ensure that code changes do not disrupt the functi
- Common Providers and Contexts from the lifecycle of our App, such as [`next-intl`][] should not be mocked but given an empty or fake context whenever possible.
- We recommend reading previous unit tests from the codebase for inspiration and code guidelines.

### General Guidelines for Playwright E2E Tests

End-to-end (E2E) tests are essential for ensuring that the entire application works correctly from a user's perspective:

- E2E tests are located in the `apps/site/tests/e2e` directory.
- We use [Playwright](https://playwright.dev/) as our E2E testing framework.
- E2E tests should focus on user flows and critical paths through the application.
- Tests should be written to be resilient to minor UI changes and should prioritize testing functionality over exact visual appearance.
- When writing E2E tests:
- Use meaningful test descriptions that clearly indicate what is being tested.
- Group related tests using Playwright's test grouping features.
- Use page objects or similar patterns to keep tests maintainable.
- Minimize test interdependencies to prevent cascading failures.
- Tests should run against the built application to accurately reflect the production environment.
- We recommend reviewing existing E2E tests in the codebase for patterns and best practices.
- If your feature involves complex user interactions or spans multiple pages, consider adding E2E tests to verify the complete flow.

### General Guidelines for Storybooks

Storybooks are an essential part of our development process. They help us to document our components and to ensure that the components are working as expected.
Expand Down
4 changes: 4 additions & 0 deletions apps/site/.stylelintignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ styles/old
# Cloudflare Build Output
.open-next
.wrangler

# Playwright
test-results
playwright-report
2 changes: 1 addition & 1 deletion apps/site/components/withNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const WithNavBar: FC = () => {

<ThemeToggle
onClick={toggleCurrentTheme}
ariaLabel={t('components.common.themeToggle.label')}
aria-label={t('components.common.themeToggle.label')}
/>

<LanguageDropdown
Expand Down
2 changes: 2 additions & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"lint:fix": "turbo run lint:md lint:js lint:css --no-cache -- --fix",
"lint:js": "eslint \"**/*.{js,mjs,ts,tsx}\"",
"lint:md": "eslint \"**/*.md?(x)\" --cache --cache-strategy=content --cache-location=.eslintmdcache",
"playwright": "playwright test",
"scripts:release-post": "cross-env NODE_NO_WARNINGS=1 node scripts/release-post/index.mjs",
"serve": "pnpm dev",
"start": "cross-env NODE_NO_WARNINGS=1 next start",
Expand Down Expand Up @@ -84,6 +85,7 @@
"@flarelabs-net/wrangler-build-time-fs-assets-polyfilling": "^0.0.0",
"@next/eslint-plugin-next": "15.3.1",
"@opennextjs/cloudflare": "^1.0.0-beta.4",
"@playwright/test": "^1.52.0",
"@testing-library/user-event": "~14.6.1",
"@types/semver": "~7.7.0",
"eslint-config-next": "15.3.1",
Expand Down
32 changes: 32 additions & 0 deletions apps/site/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { defineConfig, devices } from '@playwright/test';

const isCI = !!process.env.CI;

// https://playwright.dev/docs/test-configuration
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: isCI,
retries: isCI ? 2 : 0,
workers: isCI ? 1 : undefined,
reporter: isCI ? [['html'], ['github']] : [['html']],
use: {
baseURL: process.env.VERCEL_PREVIEW_URL || 'http://127.0.0.1:3000',
trace: 'on-first-retry',
},

projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
159 changes: 159 additions & 0 deletions apps/site/tests/e2e/general-behavior.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { importLocale } from '@node-core/website-i18n';
import { test, expect, type Page } from '@playwright/test';

const englishLocale = await importLocale('en');

// TODO(@avivkeller): It would be ideal for all the Test IDs to not exist in the
// ui-components package, and instead be passed as props.
const locators = {
// Navigation elements
mobileMenuToggleName:
englishLocale.components.containers.navBar.controls.toggle,
navLinksLocator: `[aria-label="${englishLocale.components.containers.navBar.controls.toggle}"] + div`,
// Global UI controls
languageDropdownName: englishLocale.components.common.languageDropdown.label,
themeToggleName: englishLocale.components.common.themeToggle.label,

// Search components (from Orama library)
searchButtonTag: 'orama-button',
searchInputTag: 'orama-input',
searchResultsTag: 'orama-search-results',
};

const getTheme = (page: Page) =>
page.evaluate(() => document.documentElement.dataset.theme);

const openLanguageMenu = async (page: Page) => {
const button = page.getByRole('button', {
name: locators.languageDropdownName,
});
const selector = `[aria-labelledby=${await button.getAttribute('id')}]`;
await button.click();

await page.waitForSelector(selector);
return page.locator(selector);
};

const verifyTranslation = async (
page: Page,
locale: string | Record<string, unknown>
) => {
// Load locale data if string code provided (e.g., 'es', 'fr')
const localeData =
typeof locale === 'string' ? await importLocale(locale) : locale;

// Get navigation links and expected translations
const links = await page
.locator(locators.navLinksLocator)
.locator('a > span')
.all();
const expectedTexts = Object.values(
localeData.components.containers.navBar.links
);

// Verify each navigation link text matches an expected translation
for (const link of links) {
const linkText = await link.textContent();
expect(expectedTexts).toContain(linkText!.trim());
}
};

test.describe('Node.js Website', () => {
// Start each test from the English homepage
test.beforeEach(async ({ page }) => {
await page.goto('/en');
});

test.describe('Theme', () => {
test('should toggle between light/dark themes', async ({ page }) => {
const themeToggle = page.getByRole('button', {
name: locators.themeToggleName,
});
await expect(themeToggle).toBeVisible();

const initialTheme = await getTheme(page);
await themeToggle.click();

const newTheme = await getTheme(page);
expect(newTheme).not.toEqual(initialTheme);
expect(['light', 'dark']).toContain(newTheme);
});

test('should persist theme across page navigation', async ({ page }) => {
const themeToggle = page.getByRole('button', {
name: locators.themeToggleName,
});
await themeToggle.click();
const selectedTheme = await getTheme(page);

await page.reload();
expect(await getTheme(page)).toBe(selectedTheme);
});

test('should respect system preference initially', async ({ browser }) => {
const context = await browser.newContext({ colorScheme: 'dark' });
const page = await context.newPage();

await page.goto('/en');
expect(await getTheme(page)).toBe('dark');

await context.close();
});
});

test.describe('Language', () => {
test('should correctly translate UI elements according to language files', async ({
page,
}) => {
await verifyTranslation(page, englishLocale);

// Change to Spanish and verify translations
const menu = await openLanguageMenu(page);
await menu.getByText(/español/i).click();
await page.waitForURL(/\/es$/);

await verifyTranslation(page, 'es');
});
});

test.describe('Search', () => {
test('should show and operate search functionality', async ({ page }) => {
// Open search dialog
await page.locator(locators.searchButtonTag).click();

// Verify search input is visible and enter a search term
const searchInput = page.locator(locators.searchInputTag);
await expect(searchInput).toBeVisible();
await searchInput.pressSequentially('express');

// Verify search results appear
const searchResults = page.locator(locators.searchResultsTag);
await expect(searchResults).toBeVisible();
});
});

test.describe('Navigation', () => {
test('should have functioning mobile menu on small screens', async ({
page,
}) => {
// Set mobile viewport size
await page.setViewportSize({ width: 375, height: 667 });

// Locate mobile menu toggle button and verify it's visible
const mobileToggle = page.getByRole('button', {
name: locators.mobileMenuToggleName,
});
await expect(mobileToggle).toBeVisible();

const navLinks = page.locator(locators.navLinksLocator);

// Toggle menu open and verify it's visible
await mobileToggle.click();
await expect(navLinks.first()).toBeVisible();

// Toggle menu closed and verify it's hidden
await mobileToggle.click();
await expect(navLinks.first()).not.toBeVisible();
});
});
});
2 changes: 2 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export default [
'storybook-static/**',
'**/.wrangler',
'**/.open-next',
'test-results',
'playwright-report',
],
},
{
Expand Down
Loading
Loading