1
0
mirror of synced 2025-12-19 09:57:42 -05:00
Files
docs/src/fixtures/tests/playwright-rendering.spec.ts

1296 lines
50 KiB
TypeScript

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('<article>').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('h2')).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()
})
})