From 493a3387f820c3689f131ca68a77bc20c0c1ea92 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Wed, 14 May 2025 17:36:00 -0400 Subject: [PATCH 1/5] feat(test): add playwright tests --- .github/workflows/playwright.yml | 107 ++++++++++++++ .gitignore | 5 +- COLLABORATOR_GUIDE.md | 18 +++ apps/site/.stylelintignore | 4 + apps/site/components/withNavBar.tsx | 3 +- apps/site/package.json | 2 + apps/site/playwright.config.ts | 32 ++++ apps/site/tests/e2e/general-behavior.spec.ts | 138 ++++++++++++++++++ eslint.config.js | 2 + packages/i18n/lib/index.mjs | 4 +- .../Common/LanguageDropDown/index.tsx | 8 +- .../Common/ThemeToggle/index.tsx | 19 +-- .../ui-components/Containers/NavBar/index.tsx | 3 +- pnpm-lock.yaml | 108 +++++++------- 14 files changed, 377 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 apps/site/playwright.config.ts create mode 100644 apps/site/tests/e2e/general-behavior.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000000..bbc1cb3353d53 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -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/ diff --git a/.gitignore b/.gitignore index dd97ef0e64f2d..2c2edea1406ac 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ cache # TypeScript tsconfig.tsbuildinfo - dist/ # Ignore the blog-data json that we generate during dev and build @@ -43,3 +42,7 @@ apps/site/public/blog-data.json apps/site/.open-next apps/site/.wrangler + +## Playwright +test-results +playwright-report diff --git a/COLLABORATOR_GUIDE.md b/COLLABORATOR_GUIDE.md index 6dcd3f49d5de2..857efecd5c037 100644 --- a/COLLABORATOR_GUIDE.md +++ b/COLLABORATOR_GUIDE.md @@ -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) @@ -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. diff --git a/apps/site/.stylelintignore b/apps/site/.stylelintignore index 82e0936e61013..bb4f97f4cd44d 100644 --- a/apps/site/.stylelintignore +++ b/apps/site/.stylelintignore @@ -17,3 +17,7 @@ styles/old # Cloudflare Build Output .open-next .wrangler + +# Playwright +test-results +playwright-report diff --git a/apps/site/components/withNavBar.tsx b/apps/site/components/withNavBar.tsx index 882d98bcef78f..9625faad66ced 100644 --- a/apps/site/components/withNavBar.tsx +++ b/apps/site/components/withNavBar.tsx @@ -62,8 +62,9 @@ const WithNavBar: FC = () => { + page.evaluate(() => document.documentElement.dataset.theme); + +const openLanguageMenu = async (page: Page) => { + await page.getByTestId(testIds.languageDropdown).first().click(); + await page.waitForSelector(`data-testid=${testIds.languageOptions}`); +}; + +const verifyTranslation = async (page: Page, locale: string) => { + const localeData = await importLocale(locale); + + // Get all navigation links + const links = await page.getByTestId(testIds.navLinks).locator('a').all(); + const expectedTexts = Object.values( + localeData.components.containers.navBar.links + ); + + // For each link, verify its text is in the expected translations + for (const link of links) { + const linkText = await link.textContent(); + expect(expectedTexts).toContain(linkText!.trim()); + } +}; + +test.describe('Node.js Website', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/en'); + }); + + test.describe('Theme', () => { + test('should toggle between light/dark themes', async ({ page }) => { + const themeToggle = page.getByTestId(testIds.themeToggle).first(); + 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.getByTestId(testIds.themeToggle).first(); + 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, + }) => { + // Verify English content + await verifyTranslation(page, 'en'); + + // Change to Spanish and verify + await openLanguageMenu(page); + await page + .getByTestId(testIds.languageOptions) + .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 }) => { + await page.locator(selectors.searchButton).click(); + + const searchInput = page.locator(selectors.searchInput); + await expect(searchInput).toBeVisible(); + await searchInput.pressSequentially('express'); + + const searchResults = page.locator(selectors.searchResults); + await expect(searchResults).toBeVisible(); + }); + }); + + test.describe('Navigation', () => { + test('should have functioning mobile menu on small screens', async ({ + page, + }) => { + // Set mobile viewport + await page.setViewportSize({ width: 375, height: 667 }); + + const mobileToggle = page.getByTestId(testIds.mobileMenuToggle); + await expect(mobileToggle).toBeVisible(); + + const navLinks = page.getByTestId(testIds.navLinks); + + // Toggle menu open and verify + await mobileToggle.click(); + await expect(navLinks.first()).toBeVisible(); + + // Toggle menu closed and verify + await mobileToggle.click(); + await expect(navLinks.first()).not.toBeVisible(); + }); + }); +}); diff --git a/eslint.config.js b/eslint.config.js index c0cd4b7e032c0..4359c1ca31f3e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -16,6 +16,8 @@ export default [ 'storybook-static/**', '**/.wrangler', '**/.open-next', + 'test-results', + 'playwright-report', ], }, { diff --git a/packages/i18n/lib/index.mjs b/packages/i18n/lib/index.mjs index 12956d80a643b..25336fe7330a4 100644 --- a/packages/i18n/lib/index.mjs +++ b/packages/i18n/lib/index.mjs @@ -9,7 +9,9 @@ import localeConfig from '../config.json' with { type: 'json' }; * @returns {Promise>} The imported locale */ export const importLocale = async locale => { - return import(`../locales/${locale}.json`).then(f => f.default); + return import(`../locales/${locale}.json`, { with: { type: 'json' } }).then( + f => f.default + ); }; /** diff --git a/packages/ui-components/Common/LanguageDropDown/index.tsx b/packages/ui-components/Common/LanguageDropDown/index.tsx index e55178333f431..1f897e1567f8e 100644 --- a/packages/ui-components/Common/LanguageDropDown/index.tsx +++ b/packages/ui-components/Common/LanguageDropDown/index.tsx @@ -23,7 +23,11 @@ const LanguageDropdown: FC = ({ return ( - @@ -34,7 +38,7 @@ const LanguageDropdown: FC = ({ className={styles.dropDownContent} sideOffset={5} > -
+
{availableLanguages.map(({ name, code, localName }) => ( ) => void; - ariaLabel: string; -}; - -const ThemeToggle: FC = ({ - onClick = () => {}, - ariaLabel, -}) => { +const ThemeToggle: FC> = props => { return ( - diff --git a/packages/ui-components/Containers/NavBar/index.tsx b/packages/ui-components/Containers/NavBar/index.tsx index 1c89566be1c42..95de22edb22bf 100644 --- a/packages/ui-components/Containers/NavBar/index.tsx +++ b/packages/ui-components/Containers/NavBar/index.tsx @@ -54,6 +54,7 @@ const NavBar: FC> = ({ {navInteractionIcons[isMenuOpen ? 'close' : 'show']} @@ -69,7 +70,7 @@ const NavBar: FC> = ({ />