import dotenv from 'dotenv' import { test, expect } from '@playwright/test' import { turnOffExperimentsInPage, dismissCTAPopover } from '../helpers/turn-off-experiments' // This exists for the benefit of local testing. // In GitHub Actions, we rely on setting the environment variable directly // but for convenience, for local development, engineers might have a // .env file that can set environment variable. E.g. ELASTICSEARCH_URL. // The `src/frame/start-server.ts` script uses dotenv too, but since Playwright // tests only interface with the server via HTTP, we too need to find // this out. dotenv.config({ quiet: true }) const SEARCH_TESTS = !!process.env.ELASTICSEARCH_URL test('view home page', async ({ page }) => { await page.goto('/') await expect(page).toHaveTitle(/GitHub Docs/) }) test('logo link keeps current version', async ({ page }) => { await page.goto('/enterprise-cloud@latest') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // Basically clicking into any page that isn't the home page for this version. await page.getByTestId('product').getByRole('link', { name: 'Get started' }).click() await expect(page).toHaveURL(/\/en\/enterprise-cloud@latest\/get-started/) await page.getByRole('link', { name: 'GitHub Docs' }).click() await expect(page).toHaveURL(/\/en\/enterprise-cloud@latest/) }) test('view the for-playwright article', async ({ page }) => { await page.goto('/get-started/foo/for-playwright') await expect(page).toHaveTitle(/For Playwright - GitHub Docs/) // This is the right-hand sidebar mini-toc link await page .getByTestId('minitoc') .getByRole('link', { name: 'Second heading', exact: true }) .click() await expect(page).toHaveURL(/for-playwright#second-heading/) }) test('use sidebar to go to Hello World page', async ({ page }) => { await page.goto('/get-started') await expect(page).toHaveTitle(/Getting started with HubGit/) await page.getByTestId('product-sidebar').getByText('Start your journey').click() await page.getByTestId('product-sidebar').getByText('Hello World').click() await expect(page).toHaveURL(/\/en\/get-started\/start-your-journey\/hello-world/) await expect(page).toHaveTitle(/Hello World - GitHub Docs/) }) test('do a search from home page and click on "Foo" page', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') await page.goto('/') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // Use the search overlay await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('serve playwright') // Wait for search results to load await page.waitForTimeout(1000) // Click "View more results" to get to the search page await page.getByText('View more results').click() await expect(page).toHaveURL( /\/search\?search-overlay-input=serve\+playwright&query=serve\+playwright/, ) await expect(page).toHaveTitle(/\d Search results for "serve playwright"/) await page.getByRole('link', { name: 'For Playwright' }).click() await expect(page).toHaveURL(/\/get-started\/foo\/for-playwright$/) await expect(page).toHaveTitle(/For Playwright/) }) test('open search, and perform a general search', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') await page.goto('/') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('serve playwright') // Wait for the results to load // NOTE: In the UI we wait for results to load before allowing "enter", because we don't want // to allow an unnecessary request when there are no search results. Easier to wait 1 second await page.waitForTimeout(1000) // Scroll down to "View all results" then press enter await page.getByText('View more results').click() await expect(page).toHaveURL( /\/search\?search-overlay-input=serve\+playwright&query=serve\+playwright/, ) await expect(page).toHaveTitle(/\d Search results for "serve playwright"/) // The first result should be "For Playwright" await page.getByRole('link', { name: 'For Playwright' }).click() await expect(page).toHaveURL(/\/get-started\/foo\/for-playwright$/) await expect(page).toHaveTitle(/For Playwright/) }) test('open search, and select a general search article', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') await page.goto('/') await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('serve playwright') // Let new suggestions load const searchOverlay = page.getByTestId('general-autocomplete-suggestions') await expect(searchOverlay.getByText('For Playwright')).toBeVisible() // Navigate to general search item, "For Playwright" await page.keyboard.press('ArrowDown') // Select the general search item, "For Playwright" await page.keyboard.press('Enter') // We should now be on the page for "For Playwright" await expect(page).toHaveURL(/\/get-started\/foo\/for-playwright$/) await expect(page).toHaveTitle(/For Playwright/) }) test('open search, and get auto-complete results', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') await page.goto('/') await page.locator('[data-testid="search"]:visible').click() let listGroup = page.getByTestId('ai-autocomplete-suggestions') await expect(listGroup).toBeVisible() let listItems = listGroup.locator('li') await expect(listItems).toHaveCount(4) // Top queries from queries.json fixture's 'topQueries' let expectedTexts = [ 'What is GitHub and how do I get started?', 'What is GitHub Copilot and how do I get started?', 'How do I connect to GitHub with SSH?', 'How do I generate a personal access token?', ] for (let i = 0; i < expectedTexts.length; i++) { await expect(listItems.nth(i)).toHaveText(expectedTexts[i]) } const searchInput = await page.getByTestId('overlay-search-input') await expect(searchInput).toBeVisible() await expect(searchInput).toBeEnabled() // Type the text "rest" into the search input await searchInput.fill('rest') // For for 1 second for the suggestions to load await page.waitForTimeout(1000) // Ask AI suggestions listGroup = page.getByTestId('ai-autocomplete-suggestions') listItems = listGroup.locator('li') await expect(listItems).toHaveCount(3) await expect(listGroup).toBeVisible() expectedTexts = [ 'rest', 'How do I manage OAuth app access restrictions for my organization?', 'How do I test my SSH connection to GitHub?', ] for (let i = 0; i < expectedTexts.length; i++) { await expect(listItems.nth(i)).toHaveText(expectedTexts[i]) } }) test('search from enterprise-cloud and filter by top-level Fooing', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') await page.goto('/enterprise-cloud@latest') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // Use the search overlay await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('fixture') // Wait for search results to load await page.waitForTimeout(1000) // Click "View more results" to get to the search page await page.getByText('View more results').click() // Now we're on the search results page, apply the filter await page.getByText('Fooing (1)').click() await page.getByRole('link', { name: 'Clear' }).click() // At the moment this test isn't great because it's not proving that // certain things cease to be visible, that was visible before. Room // for improvement! }) test('404 page renders correctly', async ({ page }) => { const response = await page.goto('/this-definitely-does-not-exist') expect(response?.status()).toBe(404) // Check that the 404 page content is rendered await expect(page.getByText(/It looks like this page doesn't exist/)).toBeVisible() }) test.describe('platform picker', () => { test('switch operating systems', async ({ page }) => { await page.goto('/get-started/liquid/platform-specific') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await page.getByTestId('platform-picker').getByRole('link', { name: 'Mac' }).click() await expect(page).toHaveURL(/\?platform=mac/) await expect(page.getByRole('heading', { name: /Macintosh/ })).toBeVisible() await expect(page.getByRole('heading', { name: /Windows 95/ })).not.toBeVisible() await page.getByTestId('platform-picker').getByRole('link', { name: 'Windows' }).click() await expect(page).toHaveURL(/\?platform=windows/) await expect(page.getByRole('heading', { name: /Windows 95/ })).toBeVisible() await expect(page.getByRole('heading', { name: /Macintosh/ })).not.toBeVisible() }) test('minitoc matches picker', async ({ page }) => { // default platform set to windows in fixture fronmatter await page.goto('/get-started/liquid/platform-specific') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Macintosh until 1999' }), ).not.toBeVisible() await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Windows 95 was awesome' }), ).toBeVisible() await page.getByTestId('platform-picker').getByRole('link', { name: 'Linux' }).click() await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Macintosh until 1999' }), ).not.toBeVisible() await expect( page.getByTestId('minitoc').getByRole('link', { name: 'The year of Linux on the desktop' }), ).toBeVisible() }) test('remember last clicked OS', async ({ page }) => { await page.goto('/get-started/liquid/platform-specific') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await page.getByTestId('platform-picker').getByRole('link', { name: 'Windows' }).click() // Return and now the cookie should start us off on Windows again await page.goto('/get-started/liquid/platform-specific') await expect(page.getByRole('heading', { name: /Windows 95/ })).toBeVisible() await expect(page.getByRole('heading', { name: /Macintosh/ })).not.toBeVisible() }) }) test.describe('tool picker', () => { test('switch tools', async ({ page }) => { await page.goto('/get-started/liquid/tool-specific') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await page.getByTestId('tool-picker').getByRole('link', { name: 'GitHub CLI' }).click() await expect(page).toHaveURL(/\?tool=cli/) await expect(page.getByText('This is cli content')).toBeVisible() await expect(page.getByText('This is webui content')).not.toBeVisible() await page.getByTestId('tool-picker').getByRole('link', { name: 'Web browser' }).click() await expect(page).toHaveURL(/\?tool=webui/) await expect(page.getByText('This is cli content')).not.toBeVisible() await expect(page.getByText('This is desktop content')).not.toBeVisible() await expect(page.getByText('This is webui content')).toBeVisible() }) test('prefer default tool', async ({ page }) => { await page.goto('/get-started/liquid/tool-specific') // defaultTool is set in the fixture frontmatter to webui await expect(page.getByText('This is webui content')).toBeVisible() await expect(page.getByText('This is desktop content')).not.toBeVisible() await expect(page.getByText('This is cli content')).not.toBeVisible() }) test('remember last clicked tool', async ({ page }) => { await page.goto('/get-started/liquid/tool-specific') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await page.getByTestId('tool-picker').getByRole('link', { name: 'Web browser' }).click() // Return and now the cookie should start us off with Web UI content again await page.goto('/get-started/liquid/tool-specific') await expect(page.getByText('This is cli content')).not.toBeVisible() await expect(page.getByText('This is desktop content')).not.toBeVisible() await expect(page.getByText('This is webui content')).toBeVisible() }) test('minitoc matches picker', async ({ page }) => { // default tool set to webui in fixture frontmatter await page.goto('/get-started/liquid/tool-specific') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Webui section' }), ).toBeVisible() await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Desktop section' }), ).not.toBeVisible() await page.getByTestId('tool-picker').getByRole('link', { name: 'Desktop' }).click() await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Webui section' }), ).not.toBeVisible() await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Desktop section' }), ).toBeVisible() }) }) test('navigate with side bar into article inside a subcategory inside a category', async ({ page, }) => { // Our TreeView sidebar only shows "2 levels". If you click and expand // the category, you'll be able to see the subcategory and the article // within. await page.goto('/actions') await page.getByTestId('sidebar').getByText('Category', { exact: true }).click() await page.getByTestId('sidebar').getByText('Subcategory').click() await page.getByText('
').click() await expect(page.getByRole('heading', { name: 'Article title' })).toBeVisible() await expect(page).toHaveURL(/actions\/category\/subcategory\/article/) }) test('sidebar custom link functionality works', async ({ page }) => { // Test that sidebar functionality is not broken by custom links feature await page.goto('/get-started') await expect(page).toHaveTitle(/Getting started with HubGit/) // Verify that regular sidebar navigation still works by clicking on known sections await page.getByTestId('product-sidebar').getByText('Start your journey').click() await page.getByTestId('product-sidebar').getByText('Hello World').click() await expect(page).toHaveURL(/\/en\/get-started\/start-your-journey\/hello-world/) await expect(page).toHaveTitle(/Hello World - GitHub Docs/) }) test.describe('hover cards', () => { test('hover over link', async ({ page }) => { await page.goto('/pages/quickstart') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // hover over a link and check for intro content from hovercard await page .locator('#article-contents') .getByRole('link', { name: 'Start your journey' }) .hover() await expect( page.getByText( 'Get started using HubGit to manage Git repositories and collaborate with others.', ), ).toBeVisible() // now move the mouse away from hovering over the link, the hovercard should // no longer be visible await page.mouse.move(0, 0) await expect( page.getByText( 'Get started using GitHub to manage Git repositories and collaborate with others.', ), ).not.toBeVisible() // external links don't have a hovercard await page.getByRole('link', { name: 'github.com/github/docs' }).hover() await expect(page.getByTestId('popover')).not.toBeVisible() // links in the main navigation sidebar don't have a hovercard await page.getByTestId('sidebar').getByRole('link', { name: 'Quickstart' }).hover() await expect(page.getByTestId('popover')).not.toBeVisible() // links in the secondary minitoc sidebar don't have a hovercard await page .getByTestId('minitoc') .getByRole('link', { name: 'Regular internal link', exact: true }) .hover() await expect(page.getByTestId('popover')).not.toBeVisible() // links in the article intro have a hovercard await page.locator('#article-intro').getByRole('link', { name: 'article intro link' }).hover() await expect(page.getByText('You can use HubGit Pages to showcase')).toBeVisible() // this page's intro has two links; one in-page and one internal await page.locator('#article-intro').getByRole('link', { name: 'another link' }).hover() await expect( page.getByText('Follow this Hello World exercise to get started with HubGit.'), ).toBeVisible() // same page anchor links have a hovercard await page .locator('#article-contents') .getByRole('link', { name: 'introduction', exact: true }) .hover() await expect(page.getByText('You can use HubGit Pages to showcase')).toBeVisible() // links with formatted text need to work too await page.locator('#article-contents').getByRole('link', { name: 'Bold is strong' }).hover() await expect(page.getByText('The most basic of fixture data for HubGit')).toBeVisible() await page.locator('#article-contents').getByRole('link', { name: 'bar' }).hover() await expect(page.getByText("This page doesn't really have an intro")).toBeVisible() }) test('use keyboard shortcut to open hover card', async ({ page }) => { await page.goto('/pages/quickstart') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // Simply putting focus on the link should not open the hovercard await page .locator('#article-contents') .getByRole('link', { name: 'Start your journey' }) .focus() await expect( page.getByText( 'Get started using GitHub to manage Git repositories and collaborate with others.', ), ).not.toBeVisible() // Once a link has got focus, you can use Alt+ArrowUp to open the hovercard await page.keyboard.press('Alt+ArrowUp') await expect( page.getByText( 'Get started using HubGit to manage Git repositories and collaborate with others.', ), ).toBeVisible() // Press Escape to close it await page.keyboard.press('Escape') await expect( page.getByText( 'Get started using GitHub to manage Git repositories and collaborate with others.', ), ).not.toBeVisible() }) test('able to use Esc to close hovercard', async ({ page }) => { await page.goto('/pages/quickstart') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // hover over a link and check for intro content from hovercard await page .locator('#article-contents') .getByRole('link', { name: 'Start your journey' }) .hover() await expect( page.getByText( 'Get started using HubGit to manage Git repositories and collaborate with others.', ), ).toBeVisible() // click the Esc key to close the hovercard await page.keyboard.press('Escape') await expect( page.getByText( 'Get started using GitHub to manage Git repositories and collaborate with others.', ), ).not.toBeVisible() }) }) test.describe('test nav at different viewports', () => { test('xx-large viewports - 1400+', async ({ page }) => { page.setViewportSize({ width: 1400, height: 700, }) await page.goto('/get-started/foo/bar') // in article breadcrumbs at our custom xl viewport should remove last // breadcrumb so for this page we should only have 'Get Started / Foo' expect(await page.getByTestId('breadcrumbs-in-article').getByRole('link').all()).toHaveLength(2) await expect(page.getByTestId('breadcrumbs-in-article').getByText('Foo')).toBeVisible() await expect(page.getByTestId('breadcrumbs-in-article').getByText('Bar')).not.toBeVisible() // breadcrumbs show up in rest reference pages await page.goto('/rest/actions/artifacts') await expect(page.getByTestId('breadcrumbs-in-article')).toBeVisible() // breadcrumbs show up in one of the pages that use the AutomatedPage // component (e.g. graphql, audit log, etc.) -- we test the webhooks // reference page here await page.goto('/webhooks/webhook-events-and-payloads') await expect(page.getByTestId('breadcrumbs-in-article')).toBeVisible() }) test('large -> x-large viewports - 1012+', async ({ page }) => { page.setViewportSize({ width: 1013, height: 700, }) await page.goto('/get-started/foo/bar') // version picker should be visible await page .getByRole('button', { name: 'Select GitHub product version: current version is free-pro-team@latest', }) .click() expect((await page.getByRole('menuitemradio').all()).length).toBeGreaterThan(0) await expect(page.getByRole('menuitemradio', { name: 'Enterprise Cloud' })).toBeVisible() // language picker is visible await page.getByRole('button', { name: 'Select language: current language is English' }).click() await expect(page.getByRole('menuitemradio', { name: 'English' })).toBeVisible() // header sign up button is visible await expect(page.getByTestId('header-signup')).toBeVisible() }) test('large viewports - 1012-1279', async ({ page }) => { page.setViewportSize({ width: 1013, height: 700, }) await page.goto('/get-started/foo/bar') // breadcrumbs show up in the header, for this page we should have // 3 items 'Get Started / Foo / Bar' // in-article breadcrumbs don't show up await expect(page.getByTestId('breadcrumbs-header')).toBeVisible() expect(await page.getByTestId('breadcrumbs-header').getByRole('link').all()).toHaveLength(3) await expect(page.getByTestId('breadcrumbs-in-article')).not.toBeVisible() // hamburger button for sidebar overlay is visible await expect(page.getByTestId('sidebar-hamburger')).toBeVisible() await page.getByTestId('sidebar-hamburger').click() await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible() }) test('medium viewports - 768-1011', async ({ page }) => { page.setViewportSize({ width: 1000, height: 700, }) await page.goto('/get-started/foo/bar') // version picker is visible await page .getByRole('button', { name: 'Select GitHub product version: current version is free-pro-team@latest', }) .click() expect((await page.getByRole('menuitemradio').all()).length).toBeGreaterThan(0) await expect(page.getByRole('menuitemradio', { name: 'Enterprise Cloud' })).toBeVisible() // language picker is in mobile menu await page.getByTestId('mobile-menu').click() await expect(page.getByRole('menuitemradio', { name: 'English' })).toBeVisible() // sign up button is in mobile menu await expect(page.getByTestId('mobile-signup')).toBeVisible() // hamburger button for sidebar overlay is visible await expect(page.getByTestId('sidebar-hamburger')).toBeVisible() await page.getByTestId('sidebar-hamburger').click() await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible() }) test('small viewports - 544-767', async ({ page }) => { page.setViewportSize({ width: 555, height: 700, }) await page.goto('/get-started/foo/bar') // header sign-up button is not visible await expect(page.getByTestId('header-signup')).not.toBeVisible() // language picker is not visible await expect(page.getByTestId('language-picker')).not.toBeVisible() // version picker is visible await expect( page.getByRole('button', { name: 'Select GitHub product version: current version is free-pro-team@latest', }), ).toBeVisible() // language picker is in mobile menu await page.getByTestId('mobile-menu').click() await expect(page.getByRole('menuitemradio', { name: 'English' })).toBeVisible() // sign up button is in mobile menu await expect(page.getByTestId('mobile-signup')).toBeVisible() // hamburger button for sidebar overlay is visible await expect(page.getByTestId('sidebar-hamburger')).toBeVisible() await page.getByTestId('sidebar-hamburger').click() await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible() }) test('x-small viewports - 0-544', async ({ page }) => { page.setViewportSize({ width: 345, height: 700, }) await page.goto('/get-started/foo/bar') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // header sign-up button is not visible await expect(page.getByTestId('header-signup')).not.toBeVisible() // language picker is not visible await expect(page.getByTestId('language-picker')).not.toBeVisible() // version picker is not visible await expect( page.getByRole('button', { name: 'Select GitHub product version: current version is free-pro-team@latest', }), ).not.toBeVisible() // version picker is in mobile menu await expect(page.getByTestId('version-picker')).not.toBeVisible() await page.getByTestId('mobile-menu').click() await expect(page.getByTestId('open-mobile-menu').getByTestId('version-picker')).toBeVisible() // language picker is in mobile menu await expect(page.getByTestId('open-mobile-menu').getByTestId('language-picker')).toBeVisible() // sign up button is in mobile menu await expect(page.getByTestId('mobile-signup')).toBeVisible() // hamburger button for sidebar overlay is visible await expect(page.getByTestId('sidebar-hamburger')).toBeVisible() await page.getByTestId('sidebar-hamburger').click() await expect(page.locator('[role="dialog"][class*="Header_dialog"]')).toBeVisible() }) test('do a search when the viewport is x-small', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') page.setViewportSize({ width: 500, height: 700, }) await page.goto('/get-started/foo/bar') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // Use the search overlay await page.locator('[data-testid="mobile-search-button"]:visible').click() await page.getByTestId('overlay-search-input').fill('serve playwright') // Wait for search results to load await page.waitForTimeout(1000) // Click "View more results" to get to the search page await page.getByText('View more results').click() await expect(page).toHaveURL( /\/search\?search-overlay-input=serve\+playwright&query=serve\+playwright/, ) await expect(page).toHaveTitle(/\d Search results for "serve playwright"/) }) test('do a search when the viewport is medium', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') page.setViewportSize({ width: 1000, height: 700, }) await page.goto('/get-started/foo/bar') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) // Use the search overlay await page.locator('[data-testid="mobile-search-button"]:visible').click() await page.getByTestId('overlay-search-input').fill('serve playwright') // Wait for search results to load await page.waitForTimeout(1000) // Click "View more results" to get to the search page await page.getByText('View more results').click() await expect(page).toHaveURL( /\/search\?search-overlay-input=serve\+playwright&query=serve\+playwright/, ) await expect(page).toHaveTitle(/\d Search results for "serve playwright"/) }) }) test.describe('survey', () => { test('happy path, thumbs up and enter comment and email', async ({ page }) => { let fulfilled = 0 let hasSurveyPressedEvent = false let hasSurveySubmittedEvent = false const surveyComment = 'This is a comment' // Important to set this up *before* interacting with the page // in case of possible race conditions. await page.route('**/api/events', (route, request) => { const postData = request.postData() if (postData) { const postDataArray = JSON.parse(postData) route.fulfill({}) expect(request.method()).toBe('POST') fulfilled = postDataArray.length for (const eventBody of postDataArray) { if (eventBody.type === 'survey' && eventBody.survey_vote === true) { hasSurveyPressedEvent = true } if (eventBody.type === 'survey' && eventBody.survey_vote === true) { hasSurveyPressedEvent = true } if ( eventBody.type === 'survey' && eventBody.survey_vote === true && eventBody.survey_comment === surveyComment ) { hasSurveySubmittedEvent = true } } } // At the time of writing you can't get the posted payload // when you use `navigator.sendBeacon(url, data)`. // So we can't make assertions about the payload. // See https://github.com/microsoft/playwright/issues/12231 }) await page.addInitScript(() => { window.GHDOCSPLAYWRIGHT = 1 }) await page.goto('/get-started/foo/for-playwright') // The label is visually an SVG. Finding it by its `for` value feels easier. await page.locator('[for=survey-yes]').click() await expect(page.getByRole('button', { name: 'Cancel' })).toBeVisible() await expect(page.getByRole('button', { name: 'Send' })).toBeVisible() await page.locator('[for=survey-comment]').fill(surveyComment) await page.locator('[name=survey-email]').click() await page.locator('[name=survey-email]').fill('test@example.com') await page.getByRole('button', { name: 'Send' }).click() // simulate sending an exit event to trigger sending all queued events await page.evaluate(() => { Object.defineProperty(document, 'visibilityState', { configurable: true, get() { return 'hidden' }, }) document.dispatchEvent(new Event('visibilitychange')) return new Promise((resolve) => setTimeout(resolve, 100)) }) // Events: // 1. page view event when navigating to the page // 2. Survey thumbs up event // 3. Survey submit event // 4. Exit event expect(fulfilled).toBe(1 + 1 + 1 + 1) expect(hasSurveyPressedEvent).toBe(true) expect(hasSurveySubmittedEvent).toBe(true) await expect(page.getByTestId('survey-end')).toBeVisible() }) test('thumbs up without filling in the form sends an API POST', async ({ page }) => { let fulfilled = 0 let hasSurveyEvent = false // Important to set this up *before* interacting with the page // in case of possible race conditions. await page.route('**/api/events', (route, request) => { const postData = request.postData() if (postData) { const postDataArray = JSON.parse(postData) route.fulfill({}) expect(request.method()).toBe('POST') fulfilled = postDataArray.length for (const eventBody of postDataArray) { if (eventBody.type === 'survey' && eventBody.survey_vote === true) { hasSurveyEvent = true } } } // At the time of writing you can't get the posted payload // when you use `navigator.sendBeacon(url, data)`. // So we can't make assertions about the payload. // See https://github.com/microsoft/playwright/issues/12231 }) await page.addInitScript(() => { window.GHDOCSPLAYWRIGHT = 1 }) await page.goto('/get-started/foo/for-playwright') await page.locator('[for=survey-yes]').click() // simulate sending an exit event to trigger sending all queued events await page.evaluate(() => { Object.defineProperty(document, 'visibilityState', { configurable: true, get() { return 'hidden' }, }) document.dispatchEvent(new Event('visibilitychange')) return new Promise((resolve) => setTimeout(resolve, 100)) }) // Events: // 1. page view event when navigating to the page // 2. the thumbs up click // 3. the exit event expect(fulfilled).toBe(1 + 1 + 1) expect(hasSurveyEvent).toBe(true) await expect(page.getByRole('button', { name: 'Send' })).toBeVisible() await page.getByRole('button', { name: 'Cancel' }).click() }) test('vote on one page, then go to another and it should reset', async ({ page }) => { // Important to set this up *before* interacting with the page // in case of possible race conditions. await page.route('**/api/events', (route) => { route.fulfill({}) }) await page.goto('/get-started/foo/for-playwright') await expect(page.locator('[for=survey-comment]')).not.toBeVisible() await page.locator('[for=survey-yes]').click() await expect(page.getByRole('button', { name: 'Send' })).toBeVisible() await expect(page.locator('[for=survey-comment]')).toBeVisible() await page.getByTestId('product-sidebar').getByLabel('Bar', { exact: true }).click() await expect(page.getByRole('button', { name: 'Send' })).not.toBeVisible() await expect(page.locator('[for=survey-comment]')).not.toBeVisible() }) }) test.describe('rest API reference pages', () => { test('REST actions', async ({ page }) => { await page.goto('/rest') // Before using the sidebar, make sure the page has redirected to a // URL that has that `?apiVersion=` query parameter. await expect(page).toHaveURL(/\/en\/rest\?apiVersion=/) await page.getByTestId('sidebar').getByText('Actions').click() await page.getByTestId('sidebar').getByLabel('Artifacts').click() await page.getByLabel('About artifacts in HubGit Actions').click() await expect(page).toHaveURL(/\/en\/rest\/actions\/artifacts\?apiVersion=/) await expect(page).toHaveTitle(/GitHub Actions Artifacts - GitHub Docs/) }) }) test.describe('translations', () => { test('view Japanese home page', async ({ page }) => { await page.goto('/ja') await expect(page.getByRole('heading', { name: '日本 GitHub Docs' })).toBeVisible() }) test('switch to Japanese from English using widget on home page', async ({ page }) => { await page.goto('/en') await page.getByRole('button', { name: 'Select language: current language is English' }).click() await page.getByRole('menuitemradio', { name: '日本語' }).click() await expect(page).toHaveURL('/ja') await expect(page.getByRole('heading', { name: '日本 GitHub Docs' })).toBeVisible() // Having done this once, should now use a cookie to redirect back to Japanese await page.goto('/') await expect(page).toHaveURL('/ja') }) test('switch to Japanese from English using widget on article', async ({ page }) => { await page.goto('/get-started/start-your-journey/hello-world') await expect(page).toHaveURL('/en/get-started/start-your-journey/hello-world') await page.getByRole('button', { name: 'Select language: current language is English' }).click() await page.getByRole('menuitemradio', { name: '日本語' }).click() await expect(page).toHaveURL('/ja/get-started/start-your-journey/hello-world') await expect(page.getByRole('heading', { name: 'こんにちは World' })).toBeVisible() // Having done this once, should now use a cookie to redirect // back to Japanese. // Playwright will cache this redirect, so we need to add something // to "cache bust" the URL const cb = `?cb=${Math.random()}` await page.goto(`/get-started/start-your-journey/hello-world${cb}`) await expect(page).toHaveURL(`/ja/get-started/start-your-journey/hello-world${cb}`) // If you go, with the Japanese cookie, to the English page directly, // it will offer a link to the Japanese URL in a banner. await page.goto('/en/get-started/start-your-journey/hello-world') await expect(page).toHaveURL('/ja/get-started/start-your-journey/hello-world') }) }) test('open search, and ask Copilot (Ask AI) a question', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') // Mock the CSE Copilot endpoint await page.route('**/api/ai-search/v1', async (route) => { // Simulate the streaming response from CSE Copilot const mockResponse = `{"chunkType":"SOURCES","sources":[{"title":"Creating a new repository","index":"/en/get-started","url":"http://localhost:4000/en/get-started"}]} {"chunkType":"MESSAGE_CHUNK","text":"Creating "} {"chunkType":"MESSAGE_CHUNK","text":"a "} {"chunkType":"MESSAGE_CHUNK","text":"repository "} {"chunkType":"MESSAGE_CHUNK","text":"on "} {"chunkType":"MESSAGE_CHUNK","text":"GitHub "} {"chunkType":"MESSAGE_CHUNK","text":"is "} {"chunkType":"MESSAGE_CHUNK","text":"something "} {"chunkType":"MESSAGE_CHUNK","text":"you "} {"chunkType":"MESSAGE_CHUNK","text":"should "} {"chunkType":"MESSAGE_CHUNK","text":"already "} {"chunkType":"MESSAGE_CHUNK","text":"know "} {"chunkType":"MESSAGE_CHUNK","text":"how "} {"chunkType":"MESSAGE_CHUNK","text":"to "} {"chunkType":"MESSAGE_CHUNK","text":"do "} {"chunkType":"MESSAGE_CHUNK","text":":shrug:"}` await route.fulfill({ status: 200, headers: { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked', }, body: mockResponse, }) }) await page.goto('/') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('How do I create a Repository?') // Pressing enter should ask AI the question await page.keyboard.press('Enter') // Wait for the AI response to appear await expect(page.getByText('Creating a repository on GitHub')).toBeVisible() // Verify that sources are displayed await expect(page.getByText('Creating a new repository')).toBeVisible() // Verify the full response appears await expect(page.getByText('something you should already know how to do')).toBeVisible() // Open the "Creating new repository" source link list item // Find the references section first const aiReferencesSection = page.getByTestId('ai-references') await expect(aiReferencesSection).toBeVisible() // Wait for the reference list to be populated await expect(page.getByText('Creating a new repository')).toBeVisible() }) test('open search, Ask AI returns 400 error and shows general search results', async ({ page }) => { test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') // Mock the CSE Copilot endpoint to return a 400 error await page.route('**/api/ai-search/v1', async (route) => { await route.fulfill({ status: 400, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ upstreamStatus: 400, }), }) }) await page.goto('/') await turnOffExperimentsInPage(page) await dismissCTAPopover(page) await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('foo') // Pressing enter should trigger Ask AI, get 400 error, and show general search results await page.keyboard.press('Enter') // Wait for general search results to appear await expect(page.getByRole('link', { name: 'Foo' })).toBeVisible() await expect(page.getByRole('link', { name: 'Bar' })).toBeVisible() // Wait for the AI error message to appear // This is a canned response for the 400 error await page.waitForTimeout(1000) // Wait for the AI error message to appear // Verify the AI error message appears (canned response for 400 error) await expect( page .getByRole('paragraph') .getByText( /Sorry, I'm unable to answer that question. Please try asking a different question./, ), ).toBeVisible() // Verify general search results appear above the AI section const searchResults = page.getByTestId('general-autocomplete-suggestions') const aiSection = page.locator('#ask-ai-result-container') await expect(searchResults).toBeVisible() await expect(aiSection).toBeVisible() }) test.describe('LandingCarousel component', () => { test('displays carousel on test page', async ({ page }) => { await page.goto('/get-started/carousel') const carousel = page.locator('[data-testid="landing-carousel"]') await expect(carousel).toBeVisible() // Check that article cards are present const items = page.locator('[data-testid="carousel-items"]') const cards = items.locator('a') await expect(cards.first()).toBeVisible() // Verify cards have real titles (not "Unknown Article" when article not found) const firstCardTitle = cards.first().locator('h3') await expect(firstCardTitle).toBeVisible() await expect(firstCardTitle).not.toHaveText('Unknown Article') }) test('navigation works on desktop', async ({ page }) => { await page.setViewportSize({ width: 1200, height: 800 }) await page.goto('/get-started/carousel') const carousel = page.locator('[data-testid="landing-carousel"]') await expect(carousel).toBeVisible() // Should show 3 cards on desktop const cards = carousel.locator('a') await expect(cards).toHaveCount(3) // Check for navigation buttons if there are more than 3 articles const nextButton = carousel.getByRole('button', { name: 'Next articles' }) if (await nextButton.isVisible()) { const prevButton = carousel.getByRole('button', { name: 'Previous articles' }) await expect(prevButton).toBeDisabled() // Should be disabled on first page await expect(nextButton).toBeEnabled() } }) test('responsive behavior on mobile', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }) await page.goto('/get-started/carousel') const carousel = page.locator('[data-testid="landing-carousel"]') await expect(carousel).toBeVisible() // Should show 1 card on mobile const cards = carousel.locator('a') await expect(cards).toHaveCount(1) }) }) test.describe('Journey Tracks', () => { test('displays journey tracks on landing pages', async ({ page }) => { await page.goto('/get-started/test-journey') const journeyTracks = page.locator('[data-testid="journey-tracks"]') await expect(journeyTracks).toBeVisible() // Check that at least one track is displayed const tracks = page.locator('[data-testid="journey-track"]') await expect(tracks.first()).toBeVisible() // Verify track has proper structure const firstTrack = tracks.first() await expect(firstTrack.locator('h3')).toBeVisible() // Track title await expect(firstTrack.locator('p')).toBeVisible() // Track description }) test('track expansion and collapse functionality', async ({ page }) => { await page.goto('/get-started/test-journey') const firstTrack = page.locator('[data-testid="journey-track"]').first() const expandButton = firstTrack.locator('summary') // Initially collapsed const articlesList = firstTrack.locator('[data-testid="journey-articles"]') await expect(articlesList).not.toBeVisible() await expandButton.click() await expect(articlesList).toBeVisible() const articles = articlesList.locator('li') await expect(articles.first()).toBeVisible() await expandButton.click() await expect(articlesList).not.toBeVisible() }) test('article navigation within tracks', async ({ page }) => { await page.goto('/get-started/test-journey') const firstTrack = page.locator('[data-testid="journey-track"]').first() const expandButton = firstTrack.locator('summary') await expandButton.click() // Click on first article const firstArticle = firstTrack.locator('[data-testid="journey-articles"] li a').first() await expect(firstArticle).toBeVisible() const articleTitle = await firstArticle.textContent() expect(articleTitle).toBeTruthy() expect(articleTitle!.length).toBeGreaterThan(0) }) test('preserves version in journey track links', async ({ page }) => { await page.goto('/enterprise-cloud@latest/get-started/test-journey') const firstTrack = page.locator('[data-testid="journey-track"]').first() const expandButton = firstTrack.locator('summary') await expandButton.click() // article links should preserve the language and version const firstArticle = firstTrack.locator('[data-testid="journey-articles"] li a').first() const href = await firstArticle.getAttribute('href') expect(href).toContain('/en/') expect(href).toContain('enterprise-cloud@latest') }) test('handles liquid template rendering in track content', async ({ page }) => { await page.goto('/get-started/test-journey') const tracks = page.locator('[data-testid="journey-track"]') // Check that liquid templates are rendered (no raw template syntax visible) const trackContent = await tracks.first().textContent() expect(trackContent).not.toContain('{{') expect(trackContent).not.toContain('}}') expect(trackContent).not.toContain('{%') expect(trackContent).not.toContain('%}') }) }) test.describe('LandingArticleGridWithFilter component', () => { test('displays article grid with filter controls', async ({ page }) => { await page.goto('/get-started/article-grid-discovery') // Check that the main components are visible, title, categories drop // down, search input. const articleGrid = page.getByTestId('article-grid') await expect(articleGrid).toBeVisible() const filterHeader = page.getByTestId('filter-header') await expect(filterHeader).toBeVisible() const title = page.locator('h2').filter({ hasText: 'Articles' }) await expect(title).toBeVisible() const categoryDropdown = page.getByRole('button').filter({ hasText: 'All categories' }) await expect(categoryDropdown).toBeVisible() const searchInput = page.getByPlaceholder('Search articles') await expect(searchInput).toBeVisible() }) test('displays article cards with correct content', async ({ page }) => { await page.goto('/get-started/article-grid-discovery') const articleGrid = page.getByTestId('article-grid') await expect(articleGrid).toBeVisible() // Check that article cards are present and they have expected structure // by checking the first card. const articleCards = articleGrid.getByTestId('article-card') await expect(articleCards.first()).toBeVisible() const firstCard = articleCards.first() const titleLink = firstCard.locator('h3 span') await expect(titleLink).toBeVisible() const intro = firstCard.locator('div').last() // cardIntro is the last div await expect(intro).toBeVisible() const introText = await intro.textContent() expect(introText).toBeTruthy() // Card should have categories, title, and intro, just check the card has // some text const cardText = await firstCard.textContent() expect(cardText).toBeTruthy() expect(cardText!.length).toBeGreaterThan(0) }) test('category filtering works correctly', async ({ page }) => { await page.goto('/get-started/article-grid-discovery') // Check that category dropdown button exists and is clickable const categoryDropdown = page.getByRole('button').filter({ hasText: 'All categories' }) await expect(categoryDropdown).toBeVisible() // Initially should show all articles (4 total in our fixtures) const articleGrid = page.getByTestId('article-grid') await expect(articleGrid).toBeVisible() const allArticleCards = articleGrid.getByTestId('article-card') await expect(allArticleCards).toHaveCount(4) // Click the dropdown and the 'Testing' category await categoryDropdown.click() const testingOption = page.getByText('Testing', { exact: true }).last() await expect(testingOption).toBeVisible() await testingOption.click() // After filtering by Testing category, should show only 1 article based // on our fixtures. await expect(allArticleCards).toHaveCount(1) // Verify the filtered article contains "Testing" somewhere in its markup const remainingCard = allArticleCards.first() await expect(remainingCard).toContainText('Testing') }) test('search functionality works', async ({ page }) => { await page.goto('/get-started/article-grid-discovery') const searchInput = page.getByPlaceholder('Search articles') await expect(searchInput).toBeVisible() // Initially should show all articles (4 total in our fixtures) const articleGrid = page.getByTestId('article-grid') await expect(articleGrid).toBeVisible() const articleCards = articleGrid.getByTestId('article-card') await expect(articleCards).toHaveCount(4) // Search for "Grid" - based on our fixtures, multiple articles should have "Grid" in their names await searchInput.fill('Grid') await expect(articleCards.first()).toBeVisible() // Verify that the remaining articles contain "Grid" in their content const remainingCount = await articleCards.count() expect(remainingCount).toBeGreaterThan(0) for (let i = 0; i < remainingCount; i++) { const card = articleCards.nth(i) await expect(card).toContainText('Grid') } }) test('search with no results shows appropriate message', async ({ page }) => { await page.goto('/get-started/article-grid-discovery') const searchInput = page.getByPlaceholder('Search articles') await expect(searchInput).toBeVisible() // Search for a term that definitely won't match any articles, should show // no article cards await searchInput.fill('noSuchArticles') const articleGrid = page.getByTestId('article-grid') await expect(articleGrid).toBeVisible() const articleCards = articleGrid.getByTestId('article-card') await expect(articleCards).toHaveCount(0) // Should show "no articles found" message as well const noResultsMessage = page.getByTestId('no-articles-message') await expect(noResultsMessage).toBeVisible() }) test('responsive behavior on different screen sizes', async ({ page }) => { // Super basic test, just make sure the article grid is visible on // different viewports sizes // Test desktop view (3 columns) await page.setViewportSize({ width: 1200, height: 800 }) await page.goto('/get-started/article-grid-discovery') const articleGrid = page.getByTestId('article-grid') await expect(articleGrid).toBeVisible() // Test tablet view (2 columns) await page.setViewportSize({ width: 768, height: 1024 }) await page.waitForTimeout(100) // Brief wait for responsive changes await expect(articleGrid).toBeVisible() // Test mobile view (1 column) await page.setViewportSize({ width: 375, height: 667 }) await page.waitForTimeout(100) // Brief wait for responsive changes await expect(articleGrid).toBeVisible() }) test('works with bespoke landing page', async ({ page }) => { // Other grid tests use the discovery landing page, bespoke pages are // similar so just do a quick check. await page.goto('/get-started/article-grid-bespoke') const articleGrid = page.getByTestId('article-grid') await expect(articleGrid).toBeVisible() }) })