Skip to content

Commit e037a42

Browse files
authored
Playwright test for navigation flow (#2114)
* feat(test): add playwright e2e tests for navigation system Add comprehensive end-to-end tests for the navigation system including: - Primary navigation structure and accessibility - Dropdown interactions for all menu items - Search functionality testing - Secondary navigation validation - Responsive behavior across multiple viewports - Accessibility and keyboard navigation - Error handling scenarios Includes test helpers and configuration updates to support the new test suite. * docs: remove outdated CI/CD integration section from README * test(navigation): improve test reliability and structure * chore: address comments * test(playwright): add tablet landscape viewport and remove mobile small The mobile small viewport was removed as it's no longer needed for testing, while tablet landscape was added to improve test coverage for tablet devices
1 parent 08844c6 commit e037a42

File tree

6 files changed

+776
-6
lines changed

6 files changed

+776
-6
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"test": "yarn run lint-scss && yarn run lint-python && yarn run test-python",
1717
"test-python": "python3 -m unittest discover tests",
1818
"test-js": "jest --env=jsdom",
19-
"test-marketo": "python3 -m unittest tests.test_marketo"
19+
"test-marketo": "python3 -m unittest tests.test_marketo",
20+
"test-e2e": "playwright test"
2021
},
2122
"dependencies": {
2223
"@canonical/cookie-policy": "^3.7.3",

playwright.config.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dotenv.config({
1414
* See https://playwright.dev/docs/test-configuration.
1515
*/
1616
export default defineConfig({
17-
testDir: path.join(__dirname, "tests/playwright/tests/forms"),
17+
testDir: path.join(__dirname, "tests/playwright/tests"),
1818
/* Run tests in files in parallel */
1919
fullyParallel: true,
2020
/* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -38,8 +38,13 @@ export default defineConfig({
3838
/* Configure projects for major browsers */
3939
projects: [
4040
{
41-
name: 'checkout',
42-
testMatch: "*.spec.ts",
41+
name: 'forms',
42+
testMatch: "forms/*.spec.ts",
43+
use: { ...devices['Desktop Chrome']},
44+
},
45+
{
46+
name: 'navigation',
47+
testMatch: "navigation/*.spec.ts",
4348
use: { ...devices['Desktop Chrome']},
4449
},
4550
],

templates/navigation/navigation.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
<li class="p-navigation__item">
1919
<a href="/search"
2020
class="js-search-button p-navigation__link--search-toggle"
21-
aria-label="Search"></a>
21+
aria-label="Search"
22+
id="js-search-button-mobile"></a>
2223
</li>
2324
<li class="p-navigation__item">
2425
<a href="/navigation" class="js-menu-button p-navigation__link">Menu</a>
@@ -80,7 +81,8 @@
8081
<li class="p-navigation__item is-right-shifted">
8182
<a href="#"
8283
class="js-search-button p-navigation__link--search-toggle"
83-
aria-label="Toggle search"></a>
84+
aria-label="Toggle search"
85+
id="js-search-button-desktop"></a>
8486
</li>
8587
</ul>
8688
<div class="p-navigation__search">
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { Page, expect } from "@playwright/test";
2+
3+
/**
4+
* Navigation test helper functions for Canonical.com e2e tests
5+
*/
6+
7+
export interface NavigationItem {
8+
title: string;
9+
url?: string;
10+
selector?: string;
11+
hasDropdown?: boolean;
12+
}
13+
14+
export interface ViewportSize {
15+
width: number;
16+
height: number;
17+
name: string;
18+
}
19+
20+
// Vanilla viewport sizes for responsive testing
21+
// https://vanillaframework.io/docs/settings/breakpoint-settings
22+
export const VIEWPORTS: ViewportSize[] = [
23+
{ width: 1920, height: 1080, name: "Desktop Large" },
24+
{ width: 1681, height: 768, name: "Desktop Standard" },
25+
{ width: 1036, height: 620, name: "Tablet Landscape" },
26+
{ width: 620, height: 1024, name: "Tablet Portrait" },
27+
{ width: 460, height: 667, name: "Mobile" },
28+
];
29+
30+
// Primary navigation items based on navigation.yaml analysis
31+
export const PRIMARY_NAV_ITEMS: NavigationItem[] = [
32+
{ title: "Products", selector: "#products-nav", hasDropdown: true },
33+
{ title: "Solutions", selector: "#solutions-nav", hasDropdown: true },
34+
{ title: "Partners", selector: "#partners-nav", hasDropdown: true },
35+
{ title: "Careers", selector: "#careers-nav", hasDropdown: true },
36+
{ title: "Company", selector: "#company-nav", hasDropdown: true }
37+
];
38+
39+
export const MOBILE_THRESHOLD = 1035;
40+
41+
/**
42+
* Check if an element exists and is visible on the page
43+
*/
44+
export const isElementVisible = async (page: Page, selector: string): Promise<boolean> => {
45+
try {
46+
const element = page.locator(selector);
47+
return await element.isVisible();
48+
} catch {
49+
return false;
50+
}
51+
};
52+
53+
/**
54+
* Accept cookie policy if present
55+
*/
56+
export const acceptCookiePolicy = async (page: Page): Promise<void> => {
57+
if (await isElementVisible(page, '#cookie-policy-button-accept-all')) {
58+
await page.locator('#cookie-policy-button-accept-all').click();
59+
// Wait for cookie banner to disappear
60+
await expect(page.locator('#cookie-policy-button-accept-all')).not.toBeVisible();
61+
}
62+
};
63+
64+
/**
65+
* Check if we're on mobile viewport (width < MOBILE_THRESHOLD)
66+
*/
67+
export const isMobileViewport = async (page: Page): Promise<boolean> => {
68+
const viewport = page.viewportSize();
69+
return viewport ? viewport.width < MOBILE_THRESHOLD : false;
70+
};
71+
72+
/**
73+
* Open mobile menu if on mobile viewport
74+
*/
75+
export const toggleMobileMenuIfNeeded = async (page: Page): Promise<void> => {
76+
if (await isMobileViewport(page)) {
77+
const menuButton = page.locator('.js-menu-button');
78+
if (await menuButton.isVisible()) {
79+
await menuButton.click();
80+
}
81+
}
82+
};
83+
84+
/**
85+
* Click on a navigation item
86+
*/
87+
export const clickNavigationItem = async (page: Page, selector: string): Promise<void> => {
88+
const navItem = page.locator(selector);
89+
await navItem.click();
90+
};
91+
92+
/**
93+
* Check if dropdown is open for a navigation item
94+
*/
95+
export const isDropdownOpen = async (page: Page, navItemId: string): Promise<boolean> => {
96+
const dropdownContent = page.locator(`#${navItemId.replace('-nav', '-content')}`).first();
97+
return await dropdownContent.isVisible();
98+
};
99+
100+
/**
101+
* Get all visible links in a dropdown
102+
*/
103+
export const getDropdownLinks = async (page: Page, navItemId: string): Promise<string[]> => {
104+
const dropdownContent = page.locator(`#${navItemId.replace('-nav', '-content')}`);
105+
const links = dropdownContent.locator('a[href]');
106+
const linkTexts: string[] = [];
107+
108+
const count = await links.count();
109+
for (let i = 0; i < count; i++) {
110+
const text = await links.nth(i).textContent();
111+
if (text?.trim()) {
112+
linkTexts.push(text.trim());
113+
}
114+
}
115+
116+
return linkTexts;
117+
};
118+
119+
/**
120+
* Test search functionality
121+
*/
122+
export const clickSearchButton = async (page: Page): Promise<void> => {
123+
const searchButton = page.locator((await isMobileViewport(page)) ? '#js-search-button-mobile' : '#js-search-button-desktop');
124+
await searchButton.click();
125+
};
126+
127+
export const testSearchFunctionality = async (page: Page, searchTerm: string = "ubuntu"): Promise<void> => {
128+
// Open search
129+
await clickSearchButton(page);
130+
131+
// Wait for search input to be visible
132+
await page.waitForSelector('#navigation-search', { state: 'visible' });
133+
134+
// Type search term
135+
await page.fill('#navigation-search', searchTerm);
136+
137+
// Submit search
138+
const searchButton = page.locator('.p-search-box__button');
139+
await searchButton.click();
140+
141+
// Wait for navigation to search results
142+
await page.waitForURL('**/search**');
143+
};
144+
145+
/**
146+
* Check if secondary navigation is present
147+
*/
148+
export const hasSecondaryNavigation = async (page: Page): Promise<boolean> => {
149+
return await isElementVisible(page, '#secondary-navigation');
150+
};
151+
152+
/**
153+
* Get secondary navigation items
154+
*/
155+
export const getSecondaryNavigationItems = async (page: Page): Promise<string[]> => {
156+
if (!(await hasSecondaryNavigation(page))) {
157+
return [];
158+
}
159+
160+
const secondaryNav = page.locator('#secondary-navigation .p-navigation__items a');
161+
const items: string[] = [];
162+
163+
const count = await secondaryNav.count();
164+
for (let i = 0; i < count; i++) {
165+
const text = await secondaryNav.nth(i).textContent();
166+
if (text?.trim()) {
167+
items.push(text.trim());
168+
}
169+
}
170+
171+
return items;
172+
};
173+
174+
/**
175+
* Navigate to homepage and ensure clean state
176+
*/
177+
export const navigateToHomepage = async (page: Page): Promise<void> => {
178+
await page.goto('/');
179+
await acceptCookiePolicy(page);
180+
};
181+
182+
/**
183+
* Check if page has loaded successfully
184+
*/
185+
export const verifyPageLoad = async (page: Page, expectedUrl?: string): Promise<void> => {
186+
// Check that page has loaded
187+
await page.waitForLoadState('domcontentloaded');
188+
189+
// Verify URL if provided
190+
if (expectedUrl) {
191+
await expect(page).toHaveURL(new RegExp(expectedUrl));
192+
}
193+
194+
// Check that main content is visible
195+
await expect(page.locator('main, .main-content, body')).toBeVisible();
196+
};
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Navigation E2E Tests
2+
3+
This directory contains end-to-end tests for the Canonical.com navigation system.
4+
5+
## Test Coverage
6+
7+
### Primary Navigation Tests
8+
- **Structure Validation**: Verifies all primary navigation items (Products, Solutions, Partners, Careers, Company) are present and properly structured
9+
- **Accessibility**: Tests ARIA attributes, keyboard navigation, and screen reader compatibility
10+
- **Branding**: Validates Canonical logo and branding elements
11+
12+
### Dropdown Navigation Tests
13+
- **Click Interactions**: Tests dropdown opening/closing on desktop click
14+
- **Touch Interactions**: Tests dropdown behavior on mobile/touch devices
15+
- **Link Validation**: Verifies all dropdown links are functional and lead to valid pages
16+
- **Content Verification**: Ensures dropdown content matches navigation.yaml structure
17+
- **Closing Behavior**: Verifies dropdown collapses when clicking the same item or outside the navigation
18+
- **ARIA Buttons**: Ensures dropdown buttons have `aria-controls` and `tabindex="0"`
19+
20+
### Search Functionality Tests
21+
- **Search Overlay**: Tests search interface opening/closing
22+
- **Search Execution**: Validates search functionality with various queries
23+
- **Error Handling**: Tests empty search and edge cases
24+
25+
### Secondary Navigation Tests
26+
- **Conditional Display**: Tests secondary navigation appears on appropriate pages
27+
- **Navigation Flow**: Validates navigation between secondary menu items
28+
- **Cross-linking**: Tests integration between primary and secondary navigation
29+
30+
### Responsive Behavior Tests
31+
- **Multiple Viewports**: Tests across 6 different viewport sizes:
32+
- Desktop Large (1920x1080)
33+
- Desktop Standard (1366x768)
34+
- Tablet Landscape (1024x768)
35+
- Tablet Portrait (768x1024)
36+
- Mobile (375x667)
37+
- Mobile Small (320x568)
38+
- **Mobile Menu**: Tests mobile menu toggle and interactions
39+
- **Adaptive Behavior**: Verifies navigation adapts properly to different screen sizes
40+
41+
### Performance and Loading Tests
42+
- **Error Handling**: Tests navigation behavior on slow networks and error pages
43+
- **State Management**: Verifies navigation state is maintained across page transitions
44+
45+
## Test Files
46+
47+
- `navigation-flows.spec.ts`: Main test file containing all navigation test suites
48+
- `../helpers/navigation-helpers.ts`: Helper functions and utilities for navigation testing
49+
50+
## Running the Tests
51+
52+
### Run All Navigation Tests
53+
```bash
54+
npx playwright test --project=navigation
55+
```
56+
57+
### Run Specific Test Suite
58+
```bash
59+
npx playwright test --project=navigation --grep "Primary Navigation"
60+
```
61+
62+
### Run Tests in Headed Mode (with browser UI)
63+
```bash
64+
npx playwright test --project=navigation --headed
65+
```
66+
67+
### Run Tests with Debug Mode
68+
```bash
69+
npx playwright test --project=navigation --debug
70+
```
71+
72+
### Generate Test Report
73+
```bash
74+
npx playwright test --project=navigation --reporter=html
75+
```
76+
77+
## Test Data Sources
78+
79+
The tests are based on the actual navigation configuration files:
80+
- `navigation.yaml`: Primary navigation structure and dropdown content
81+
- `secondary-navigation.yaml`: Secondary navigation definitions for specific pages
82+
83+
## Helper Functions
84+
85+
The navigation tests use specialized helper functions located in `../helpers/navigation-helpers.ts`:
86+
87+
Core data and constants:
88+
- `PRIMARY_NAV_ITEMS`: Canonical primary navigation items and selectors
89+
- `VIEWPORTS`: Common viewport configurations for responsive testing
90+
- `MOBILE_THRESHOLD`: Width used to branch desktop vs. mobile behavior
91+
92+
Navigation helpers:
93+
- `acceptCookiePolicy()`: Handles cookie consent banner
94+
- `navigateToHomepage()`: Navigates to the homepage and prepares clean state
95+
- `isElementVisible()`: Convenience visibility check for selectors
96+
- `isMobileViewport()`: Returns true if current viewport is mobile-sized
97+
- `toggleMobileMenuIfNeeded()`: Toggles the mobile menu when in mobile viewport
98+
- `clickNavigationItem()`: Clicks a primary navigation item
99+
- `isDropdownOpen()`: Checks if a dropdown is currently expanded
100+
- `getDropdownLinks()`: Extracts dropdown link texts (first few links validated for href)
101+
- `clickSearchButton()`: Opens the search overlay via the toolbar button
102+
- `testSearchFunctionality()`: Performs a search and validates navigation
103+
- `hasSecondaryNavigation()` / `getSecondaryNavigationItems()`: Secondary menu helpers
104+
- `verifyPageLoad()`: Validates successful page transitions
105+
106+
## Viewport Testing
107+
108+
The tests include comprehensive responsive testing across multiple device sizes:
109+
- Desktop environments (click-based interactions)
110+
- Tablet environments (mixed touch/mouse interactions)
111+
- Mobile environments (touch-based interactions, mobile menu)
112+
113+
Threshold-based behavior:
114+
- Desktop vs. mobile interactions are determined using `MOBILE_THRESHOLD` (currently `1035` pixels)
115+
116+
## Accessibility Testing
117+
118+
Navigation tests include accessibility validation:
119+
- ARIA attributes and labels
120+
- Primary menu items have `role="menuitem"`; submenu items do not
121+
- Keyboard navigation support
122+
- Focus management
123+
- Screen reader compatibility
124+
125+
## Maintenance
126+
127+
When updating navigation structure:
128+
1. Update `navigation.yaml` or `secondary-navigation.yaml` as needed
129+
2. Run the navigation tests to ensure no regressions
130+
3. Update test expectations if navigation structure changes significantly
131+
4. Add new test cases for any new navigation features
132+
133+
### Debug Tips
134+
- Use `--headed` mode to see browser interactions
135+
- Add `await page.pause()` in tests to inspect page state
136+
- Check network tab for failed requests that might affect navigation
137+
- Verify base URL configuration matches your development environment

0 commit comments

Comments
 (0)