From 9a9e2fa3390cfe49a21c8ec99341fdc65be60044 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:29:04 +0000 Subject: [PATCH 1/4] Initial plan From b552e0f338a6202572cf7f9fc3f2db9c2db6faa3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:32:46 +0000 Subject: [PATCH 2/4] Add Playwright testing infrastructure and initial tests Co-authored-by: cmaneu <790974+cmaneu@users.noreply.github.com> --- .github/workflows/pull-request.yml | 19 +++++ .gitignore | 5 ++ package-lock.json | 71 +++++++++++++++- package.json | 13 ++- playwright.config.ts | 64 +++++++++++++++ tests/homepage.spec.ts | 91 ++++++++++++++++++++ tests/workshop.spec.ts | 128 +++++++++++++++++++++++++++++ 7 files changed, 386 insertions(+), 5 deletions(-) create mode 100644 playwright.config.ts create mode 100644 tests/homepage.spec.ts create mode 100644 tests/workshop.spec.ts diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6071e58b..e9c0ffa5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -25,3 +25,22 @@ jobs: run: npm run build:cli - name: Run linters & tests run: npm test --if-present --workspaces + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + - name: Run Playwright tests + run: npm test + - name: Upload Playwright Report + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: test-results + path: test-results/ + retention-days: 30 + diff --git a/.gitignore b/.gitignore index 4ab8c347..6f2acfca 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ TODO *.env .vscode/ltex* + +# Playwright +playwright-report/ +test-results/ + diff --git a/package-lock.json b/package-lock.json index e8668c09..9da647b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,11 @@ "license": "MIT", "workspaces": [ "packages/*" - ] + ], + "devDependencies": { + "@playwright/test": "^1.57.0", + "playwright": "^1.57.0" + } }, "node_modules/@actions/core": { "version": "2.0.2", @@ -3718,6 +3722,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -19300,6 +19320,53 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/plur": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz", @@ -27664,7 +27731,7 @@ }, "packages/cli": { "name": "@moaw/cli", - "version": "1.5.1", + "version": "1.5.2", "license": "MIT", "dependencies": { "asciidoctor": "^3.0.2", diff --git a/package.json b/package.json index c00dd910..cb71eb32 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "build:website": "npm run build --workspace=website", "build:cli": "npm run build --workspace=@moaw/cli", "create:db": "npm run create:db --workspace=database", - "format": "npm run format --workspaces --if-present" + "format": "npm run format --workspaces --if-present", + "test": "playwright test", + "test:headed": "playwright test --headed", + "test:ui": "playwright test --ui" }, "repository": { "type": "git", @@ -24,5 +27,9 @@ "homepage": "https://github.com/microsoft/moaw#readme", "workspaces": [ "packages/*" - ] -} \ No newline at end of file + ], + "devDependencies": { + "@playwright/test": "^1.57.0", + "playwright": "^1.57.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..dc31161f --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,64 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * 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: [ + ['html', { outputFolder: 'playwright-report' }], + ['json', { outputFile: 'test-results/results.json' }], + ['list'] + ], + /* 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:4200', + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + /* Take screenshot on failure */ + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // Uncomment to test on other browsers + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm start', + url: 'http://localhost:4200', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); diff --git a/tests/homepage.spec.ts b/tests/homepage.spec.ts new file mode 100644 index 00000000..a92c044c --- /dev/null +++ b/tests/homepage.spec.ts @@ -0,0 +1,91 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Homepage and Workshop List', () => { + test('homepage should load successfully', async ({ page }) => { + // Navigate to the homepage + await page.goto('/'); + + // Wait for the page to be fully loaded + await page.waitForLoadState('networkidle'); + + // Check that the page title is present + await expect(page).toHaveTitle(/MOAW/); + + // Check that main content is visible + const mainContent = page.locator('app-root'); + await expect(mainContent).toBeVisible(); + }); + + test('should display workshop catalog', async ({ page }) => { + // Navigate to the homepage + await page.goto('/'); + + // Wait for the page to be fully loaded + await page.waitForLoadState('networkidle'); + + // Look for catalog/workshop list elements + // The catalog should be visible on the homepage + const catalog = page.locator('app-catalog, app-home'); + await expect(catalog).toBeVisible(); + }); + + test('should be able to search/filter workshops', async ({ page }) => { + // Navigate to the homepage + await page.goto('/'); + + // Wait for the page to be fully loaded + await page.waitForLoadState('networkidle'); + + // Look for search input or filter controls + const searchInput = page.locator('input[type="search"], input[placeholder*="Search"], input[placeholder*="search"]'); + + // If search exists, test it + if (await searchInput.count() > 0) { + await searchInput.first().fill('azure'); + await page.waitForTimeout(500); // Wait for search to filter + + // Check that results are displayed + const catalogContent = page.locator('app-catalog, app-home'); + await expect(catalogContent).toBeVisible(); + } + }); + + test('should navigate to workshop list/catalog', async ({ page }) => { + // Navigate to the homepage + await page.goto('/'); + + // Wait for the page to be fully loaded + await page.waitForLoadState('networkidle'); + + // Check if there's a link to browse all workshops or catalog + const catalogLink = page.locator('a[href*="catalog"], a:has-text("Browse"), a:has-text("Workshops")').first(); + + if (await catalogLink.count() > 0) { + await catalogLink.click(); + + // Wait for navigation + await page.waitForLoadState('networkidle'); + + // Verify we're on the catalog page + expect(page.url()).toContain('catalog'); + } + }); + + test('should display workshop cards or list items', async ({ page }) => { + // Navigate to the homepage + await page.goto('/'); + + // Wait for the page to be fully loaded + await page.waitForLoadState('networkidle'); + + // Wait a bit for workshops to load + await page.waitForTimeout(1000); + + // Check for workshop items (cards, list items, etc.) + const workshopItems = page.locator('[class*="workshop"], [class*="card"], .list-item, article'); + + // There should be at least some content + const count = await workshopItems.count(); + expect(count).toBeGreaterThan(0); + }); +}); diff --git a/tests/workshop.spec.ts b/tests/workshop.spec.ts new file mode 100644 index 00000000..b5a6b821 --- /dev/null +++ b/tests/workshop.spec.ts @@ -0,0 +1,128 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Workshop Rendering and Navigation', () => { + test('should load a workshop page', async ({ page }) => { + // Navigate to a workshop - using a generic workshop path + // The actual workshop URL might be something like /workshop/?src=sample-workshop/ + await page.goto('/workshop/'); + + // Wait for the page to load + await page.waitForLoadState('networkidle'); + + // Check that the workshop component is present + const workshopContent = page.locator('app-workshop, app-deck, app-page'); + await expect(workshopContent).toBeVisible(); + }); + + test('should render workshop content from URL parameter', async ({ page }) => { + // Try to load a specific workshop if one exists + // First, let's go to homepage and try to find a workshop link + await page.goto('/'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + // Look for any workshop link + const workshopLink = page.locator('a[href*="/workshop/"]').first(); + + if (await workshopLink.count() > 0) { + await workshopLink.click(); + await page.waitForLoadState('networkidle'); + + // Verify workshop content is rendered + const workshopContent = page.locator('app-workshop, app-deck, app-page'); + await expect(workshopContent).toBeVisible(); + + // Check that there's actual content (markdown rendered) + const content = page.locator('article, .content, .markdown, main'); + await expect(content).toBeVisible(); + } + }); + + test('should navigate between workshop sections', async ({ page }) => { + // Navigate to homepage first to find a workshop + await page.goto('/'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + // Find and click on a workshop link + const workshopLink = page.locator('a[href*="/workshop/"]').first(); + + if (await workshopLink.count() > 0) { + await workshopLink.click(); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + // Look for navigation elements (next/previous buttons, section links, etc.) + const nextButton = page.locator('button:has-text("Next"), a:has-text("Next"), [aria-label*="next"]'); + const prevButton = page.locator('button:has-text("Previous"), button:has-text("Prev"), a:has-text("Previous"), [aria-label*="previous"]'); + const tableOfContents = page.locator('nav, aside, [class*="toc"], [class*="navigation"]'); + + // Check if navigation exists + const hasNext = await nextButton.count() > 0; + const hasPrev = await prevButton.count() > 0; + const hasToc = await tableOfContents.count() > 0; + + // If navigation exists, test it + if (hasNext) { + const currentUrl = page.url(); + await nextButton.first().click(); + await page.waitForLoadState('networkidle'); + + // Verify URL changed or content updated + const newUrl = page.url(); + // URL might change or content might update without URL change + // Just verify the page is still functional + const workshopContent = page.locator('app-workshop, app-deck, app-page'); + await expect(workshopContent).toBeVisible(); + } + + if (hasToc) { + // If there's a table of contents, verify it's clickable + const tocLinks = tableOfContents.locator('a, button'); + if (await tocLinks.count() > 0) { + expect(await tocLinks.count()).toBeGreaterThan(0); + } + } + } + }); + + test('should handle workshop URL parameters correctly', async ({ page }) => { + // Test direct navigation with src parameter + await page.goto('/workshop/?src=test'); + await page.waitForLoadState('networkidle'); + + // Page should load even if workshop doesn't exist (might show error or empty state) + const workshopComponent = page.locator('app-workshop, app-root'); + await expect(workshopComponent).toBeVisible(); + }); + + test('should display workshop metadata', async ({ page }) => { + // Navigate to homepage and find a workshop + await page.goto('/'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + const workshopLink = page.locator('a[href*="/workshop/"]').first(); + + if (await workshopLink.count() > 0) { + await workshopLink.click(); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + + // Look for metadata elements (title, author, duration, etc.) + const title = page.locator('h1, .title, [class*="workshop-title"]'); + + // At minimum, there should be a title + if (await title.count() > 0) { + await expect(title.first()).toBeVisible(); + } + + // Check for content sections + const sections = page.locator('section, article, .section'); + const sectionCount = await sections.count(); + + // Should have at least some content structure + expect(sectionCount).toBeGreaterThanOrEqual(0); + } + }); +}); From aa983e78fe1a6a9990b123a20ec74522d343f98b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:44:15 +0000 Subject: [PATCH 3/4] Fix Playwright tests to properly test homepage and workshop functionality Co-authored-by: cmaneu <790974+cmaneu@users.noreply.github.com> --- tests/homepage.spec.ts | 73 +++++++++--------- tests/workshop.spec.ts | 163 ++++++++++++++++++++++++----------------- 2 files changed, 129 insertions(+), 107 deletions(-) diff --git a/tests/homepage.spec.ts b/tests/homepage.spec.ts index a92c044c..f032c6c9 100644 --- a/tests/homepage.spec.ts +++ b/tests/homepage.spec.ts @@ -11,43 +11,41 @@ test.describe('Homepage and Workshop List', () => { // Check that the page title is present await expect(page).toHaveTitle(/MOAW/); - // Check that main content is visible - const mainContent = page.locator('app-root'); - await expect(mainContent).toBeVisible(); + // Check that the main heading is visible (from home component) + const heading = page.locator('h1'); + await expect(heading).toBeVisible(); + await expect(heading).toContainText('Hands-on tutorials'); }); test('should display workshop catalog', async ({ page }) => { - // Navigate to the homepage - await page.goto('/'); + // Navigate to the catalog page + await page.goto('/catalog/'); // Wait for the page to be fully loaded await page.waitForLoadState('networkidle'); - // Look for catalog/workshop list elements - // The catalog should be visible on the homepage - const catalog = page.locator('app-catalog, app-home'); - await expect(catalog).toBeVisible(); + // Check for workshop cards or search input + const searchInput = page.locator('input[type="search"]'); + await expect(searchInput).toBeVisible(); }); test('should be able to search/filter workshops', async ({ page }) => { - // Navigate to the homepage - await page.goto('/'); + // Navigate to the catalog page + await page.goto('/catalog/'); // Wait for the page to be fully loaded await page.waitForLoadState('networkidle'); - // Look for search input or filter controls - const searchInput = page.locator('input[type="search"], input[placeholder*="Search"], input[placeholder*="search"]'); + // Look for search input + const searchInput = page.locator('input[type="search"]'); + await expect(searchInput).toBeVisible(); + + // Test search functionality + await searchInput.fill('azure'); + await page.waitForTimeout(500); // Wait for search to filter - // If search exists, test it - if (await searchInput.count() > 0) { - await searchInput.first().fill('azure'); - await page.waitForTimeout(500); // Wait for search to filter - - // Check that results are displayed - const catalogContent = page.locator('app-catalog, app-home'); - await expect(catalogContent).toBeVisible(); - } + // Verify search input has the value + await expect(searchInput).toHaveValue('azure'); }); test('should navigate to workshop list/catalog', async ({ page }) => { @@ -58,22 +56,21 @@ test.describe('Homepage and Workshop List', () => { await page.waitForLoadState('networkidle'); // Check if there's a link to browse all workshops or catalog - const catalogLink = page.locator('a[href*="catalog"], a:has-text("Browse"), a:has-text("Workshops")').first(); + const catalogLink = page.locator('a[href*="catalog"]').first(); + await expect(catalogLink).toBeVisible(); - if (await catalogLink.count() > 0) { - await catalogLink.click(); - - // Wait for navigation - await page.waitForLoadState('networkidle'); - - // Verify we're on the catalog page - expect(page.url()).toContain('catalog'); - } + await catalogLink.click(); + + // Wait for navigation + await page.waitForLoadState('networkidle'); + + // Verify we're on the catalog page + expect(page.url()).toContain('catalog'); }); test('should display workshop cards or list items', async ({ page }) => { - // Navigate to the homepage - await page.goto('/'); + // Navigate to the catalog page + await page.goto('/catalog/'); // Wait for the page to be fully loaded await page.waitForLoadState('networkidle'); @@ -81,11 +78,11 @@ test.describe('Homepage and Workshop List', () => { // Wait a bit for workshops to load await page.waitForTimeout(1000); - // Check for workshop items (cards, list items, etc.) - const workshopItems = page.locator('[class*="workshop"], [class*="card"], .list-item, article'); + // Check for workshop cards - based on catalog component structure + const workshopCards = page.locator('app-card'); - // There should be at least some content - const count = await workshopItems.count(); + // There should be at least some workshop cards + const count = await workshopCards.count(); expect(count).toBeGreaterThan(0); }); }); diff --git a/tests/workshop.spec.ts b/tests/workshop.spec.ts index b5a6b821..47c2b85c 100644 --- a/tests/workshop.spec.ts +++ b/tests/workshop.spec.ts @@ -2,87 +2,115 @@ import { test, expect } from '@playwright/test'; test.describe('Workshop Rendering and Navigation', () => { test('should load a workshop page', async ({ page }) => { - // Navigate to a workshop - using a generic workshop path - // The actual workshop URL might be something like /workshop/?src=sample-workshop/ - await page.goto('/workshop/'); - - // Wait for the page to load + // Navigate to the catalog to find an actual workshop + await page.goto('/catalog/'); await page.waitForLoadState('networkidle'); + await page.waitForTimeout(500); + + // Look for any workshop card link + const workshopLink = page.locator('app-card a').first(); + + const linkCount = await workshopLink.count(); + if (linkCount > 0) { + // Click on a workshop + await workshopLink.click(); + await page.waitForLoadState('networkidle'); - // Check that the workshop component is present - const workshopContent = page.locator('app-workshop, app-deck, app-page'); - await expect(workshopContent).toBeVisible(); + // Check that the workshop component is present + const workshopContent = page.locator('app-workshop'); + const count = await workshopContent.count(); + expect(count).toBeGreaterThan(0); + } else { + // If no workshops available, just verify the workshop route is accessible + await page.goto('/workshop/?src=test'); + await page.waitForLoadState('networkidle'); + + // At least the page should load + await expect(page).toHaveTitle(/MOAW/); + } }); test('should render workshop content from URL parameter', async ({ page }) => { // Try to load a specific workshop if one exists - // First, let's go to homepage and try to find a workshop link - await page.goto('/'); + // First, let's go to catalog and try to find a workshop link + await page.goto('/catalog/'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); - // Look for any workshop link - const workshopLink = page.locator('a[href*="/workshop/"]').first(); + // Look for any workshop card link + const workshopLink = page.locator('app-card a').first(); - if (await workshopLink.count() > 0) { + const linkCount = await workshopLink.count(); + if (linkCount > 0) { await workshopLink.click(); await page.waitForLoadState('networkidle'); - // Verify workshop content is rendered - const workshopContent = page.locator('app-workshop, app-deck, app-page'); - await expect(workshopContent).toBeVisible(); + // Verify workshop component exists + const workshopContent = page.locator('app-workshop'); + expect(await workshopContent.count()).toBeGreaterThan(0); - // Check that there's actual content (markdown rendered) - const content = page.locator('article, .content, .markdown, main'); - await expect(content).toBeVisible(); + // Check that there's markdown content loaded + const content = page.locator('markdown'); + const contentCount = await content.count(); + expect(contentCount).toBeGreaterThan(0); + } else { + // Skip test if no workshops are available + test.skip(); } }); test('should navigate between workshop sections', async ({ page }) => { - // Navigate to homepage first to find a workshop - await page.goto('/'); + // Navigate to catalog first to find a workshop + await page.goto('/catalog/'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); - // Find and click on a workshop link - const workshopLink = page.locator('a[href*="/workshop/"]').first(); + // Find and click on a workshop card + const workshopLink = page.locator('app-card a').first(); - if (await workshopLink.count() > 0) { + const linkCount = await workshopLink.count(); + if (linkCount > 0) { await workshopLink.click(); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); - // Look for navigation elements (next/previous buttons, section links, etc.) - const nextButton = page.locator('button:has-text("Next"), a:has-text("Next"), [aria-label*="next"]'); - const prevButton = page.locator('button:has-text("Previous"), button:has-text("Prev"), a:has-text("Previous"), [aria-label*="previous"]'); - const tableOfContents = page.locator('nav, aside, [class*="toc"], [class*="navigation"]'); + // Look for navigation elements (pagination, sidebar links, etc.) + const pagination = page.locator('app-pagination'); + const sidebar = page.locator('app-sidebar'); // Check if navigation exists - const hasNext = await nextButton.count() > 0; - const hasPrev = await prevButton.count() > 0; - const hasToc = await tableOfContents.count() > 0; - - // If navigation exists, test it - if (hasNext) { - const currentUrl = page.url(); - await nextButton.first().click(); - await page.waitForLoadState('networkidle'); - - // Verify URL changed or content updated - const newUrl = page.url(); - // URL might change or content might update without URL change - // Just verify the page is still functional - const workshopContent = page.locator('app-workshop, app-deck, app-page'); - await expect(workshopContent).toBeVisible(); + const hasPagination = await pagination.count() > 0; + const hasSidebar = await sidebar.count() > 0; + + // At least one of these should exist + expect(hasPagination || hasSidebar).toBe(true); + + // If pagination exists, test it + if (hasPagination) { + const paginationButtons = pagination.locator('button, a'); + if (await paginationButtons.count() > 0) { + const lastButton = paginationButtons.last(); + if (await lastButton.isEnabled()) { + await lastButton.click(); + await page.waitForTimeout(500); + + // Verify the page is still functional + const workshopContent = page.locator('app-workshop'); + expect(await workshopContent.count()).toBeGreaterThan(0); + } + } } - if (hasToc) { - // If there's a table of contents, verify it's clickable - const tocLinks = tableOfContents.locator('a, button'); - if (await tocLinks.count() > 0) { - expect(await tocLinks.count()).toBeGreaterThan(0); + // If sidebar exists, verify it has links + if (hasSidebar) { + const sidebarLinks = sidebar.locator('a'); + if (await sidebarLinks.count() > 0) { + expect(await sidebarLinks.count()).toBeGreaterThan(0); } } + } else { + // Skip test if no workshops are available + test.skip(); } }); @@ -91,38 +119,35 @@ test.describe('Workshop Rendering and Navigation', () => { await page.goto('/workshop/?src=test'); await page.waitForLoadState('networkidle'); - // Page should load even if workshop doesn't exist (might show error or empty state) - const workshopComponent = page.locator('app-workshop, app-root'); - await expect(workshopComponent).toBeVisible(); + // Page should load the workshop component + const workshopComponent = page.locator('app-workshop'); + expect(await workshopComponent.count()).toBeGreaterThan(0); }); test('should display workshop metadata', async ({ page }) => { - // Navigate to homepage and find a workshop - await page.goto('/'); + // Navigate to catalog and find a workshop + await page.goto('/catalog/'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); - const workshopLink = page.locator('a[href*="/workshop/"]').first(); + const workshopLink = page.locator('app-card a').first(); - if (await workshopLink.count() > 0) { + const linkCount = await workshopLink.count(); + if (linkCount > 0) { await workshopLink.click(); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); - // Look for metadata elements (title, author, duration, etc.) - const title = page.locator('h1, .title, [class*="workshop-title"]'); - - // At minimum, there should be a title - if (await title.count() > 0) { - await expect(title.first()).toBeVisible(); - } + // Look for header component which should contain metadata + const header = page.locator('app-header'); + expect(await header.count()).toBeGreaterThan(0); - // Check for content sections - const sections = page.locator('section, article, .section'); - const sectionCount = await sections.count(); - - // Should have at least some content structure - expect(sectionCount).toBeGreaterThanOrEqual(0); + // Check for workshop container + const workshopContainer = page.locator('app-workshop'); + expect(await workshopContainer.count()).toBeGreaterThan(0); + } else { + // Skip test if no workshops are available + test.skip(); } }); }); From 5775bbf63ea8d21e8e773e28aff1793d9d5b3eef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:46:14 +0000 Subject: [PATCH 4/4] Address code review feedback: improve test assertions and add browser caching Co-authored-by: cmaneu <790974+cmaneu@users.noreply.github.com> --- .github/workflows/pull-request.yml | 13 +++++++++++++ playwright.config.ts | 2 +- tests/workshop.spec.ts | 5 ++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index e9c0ffa5..572f54ce 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -25,8 +25,21 @@ jobs: run: npm run build:cli - name: Run linters & tests run: npm test --if-present --workspaces + - name: Get Playwright version + id: playwright-version + run: echo "version=$(npm list @playwright/test --depth=0 --json | jq -r '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT + - name: Cache Playwright browsers + uses: actions/cache@v4 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} - name: Install Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps chromium + - name: Install Playwright system dependencies + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium - name: Run Playwright tests run: npm test - name: Upload Playwright Report diff --git a/playwright.config.ts b/playwright.config.ts index dc31161f..1e0141ec 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -29,7 +29,7 @@ export default defineConfig({ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: 'http://localhost:4200', - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + /* Collect trace on first retry to reduce overhead on successful runs */ trace: 'on-first-retry', /* Take screenshot on failure */ screenshot: 'only-on-failure', diff --git a/tests/workshop.spec.ts b/tests/workshop.spec.ts index 47c2b85c..f0834596 100644 --- a/tests/workshop.spec.ts +++ b/tests/workshop.spec.ts @@ -89,6 +89,7 @@ test.describe('Workshop Rendering and Navigation', () => { if (hasPagination) { const paginationButtons = pagination.locator('button, a'); if (await paginationButtons.count() > 0) { + // Click the last button (typically "Next" or last page) const lastButton = paginationButtons.last(); if (await lastButton.isEnabled()) { await lastButton.click(); @@ -104,9 +105,7 @@ test.describe('Workshop Rendering and Navigation', () => { // If sidebar exists, verify it has links if (hasSidebar) { const sidebarLinks = sidebar.locator('a'); - if (await sidebarLinks.count() > 0) { - expect(await sidebarLinks.count()).toBeGreaterThan(0); - } + expect(await sidebarLinks.count()).toBeGreaterThan(0); } } else { // Skip test if no workshops are available