Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,7 @@ vendor
Gemfile.lock

_data/token.yml

# Output from automated tests
/playwright-report
/test-results
4 changes: 4 additions & 0 deletions _config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ exclude:
- docker-compose.yml
- Gruntfile.js
- dist/
- e2e/
- playwright-report/
- test-results/
- package.json
- package-lock.json
- playwright.config.js
- LICENSE
- CONDE_OF_CONDUCT.md
- CONTRIBUTING.md
Expand Down
155 changes: 155 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Playwright end-to-end tests

This folder contains automated browser tests for the Canada.ca Search UI.

## Recommended file structure

Keep test files organized by user workflow or page type.

Suggested starting point:

```text
e2e/
README.md
search.spec.js
query-suggestions.spec.js
advanced-search.spec.js
contextual-search.spec.js
templates.spec.js
analytics.spec.js
```

If repeated setup becomes noisy, add small helper files under `e2e/support/`. Good helper candidates:

- `goToTestPage(page, 'qs-en.html')`;
- `searchInput(page)`;
- `submitSearch(page)`;
- mock of Coveo requests/responses (if specific results are needed)

Start simple and add as needed, when it removes meaningful repetition.

## Writing tests

Add `.spec.js` files to this directory. Playwright picks them up automatically.

```js
// @ts-check
const { test, expect } = require('@playwright/test');

test('shows the search form', async ({ page }) => {
await page.goto('/tests/qs-en.html');

await expect(page.getByLabel('Search Government of Canada websites')).toBeVisible();
await expect(page.getByRole('button', { name: 'Search' })).toBeVisible();
});
```

Prefer:

- `getByRole`, `getByLabel`, `getByText`, and `getByPlaceholder`;
- web-first assertions such as `await expect(locator).toBeVisible()`;
- one user behaviour per test;
- test names that describe the expected behaviour;
- small setup repeated in each test when it makes the test clearer;
- `test.beforeEach` to reset the page state between tests.

Avoid:

- brittle CSS selectors when an accessible locator is available;
- testing internal functions from `src/connector.js`;
- use `page.waitForResponse` to wait for network requests to complete before performing tests. i.e.,
- `await page.waitForResponse(res => res.url().includes('/querySuggest'), { timeout: 5000 }).catch(() => null);`
- for other fixed sleeps such as `waitForTimeout`;
- tests that depend on another test running first;
- broad tests that cover many behaviours at once;

## Getting going

### Install dependencies:

```bash
npm install
npx playwright install chromium
```

### Start environment

The README at the repo root explains how to start the Docker-based local environment, using a valid search token.

The Playwright config expects:

```text
http://localhost:4000
```

## Running tests

Most tests can be run via `npm`:

Run all tests, silently:

```bash
npm test
```

Run the interactive Playwright UI:

```bash
npm run test:ui
```

Open the last HTML report:

```bash
npm run test:report
```

Run one file:

```bash
npx playwright test e2e/search.spec.js
```

Run one test by title:

```bash
npx playwright test -g "renders the search form"
```

## Configuration

The Playwright config lives at `playwright.config.js`.

Current defaults:

- tests are read from `e2e/`
- the base URL is `http://localhost:4000`
- tests are run in Chromium/Playwright UI
- generated artifacts such as `playwright-report/` have been excluded from version control and the Jekyll build process

Optional configuration improvements:

- add Firefox and WebKit projects once the Chromium suite is stable;

## Debugging test failures

Use the Playwright UI while writing, debugging, or to review actual browser results:

```bash
npm run test:ui
```

Use trace mode when a failure needs a step-by-step replay:

```bash
npx playwright test --trace on
npm run test:report
```

The trace viewer is useful for failures because it shows the DOM, console, network requests, actions, and assertions around the failure.

## References

- [Playwright best practices](https://playwright.dev/docs/best-practices)
- [Playwright configuration](https://playwright.dev/docs/test-configuration)
- [Playwright continuous integration](https://playwright.dev/docs/ci)
111 changes: 111 additions & 0 deletions e2e/qs-en.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { test, expect } from '@playwright/test';

test.describe('QS EN page', () => {
test.beforeEach(async ({ page }) => {
// Mask the automation flag that Playwright sets — Coveo detects it and skips initialization.
await page.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
});

await page.goto('http://localhost:4000/tests/srb-en.html');
// await page.goto('http://localhost:4000/tests/qs-en.html');
});

test('query suggestion UI is initialized', async ({ page }) => {
await page.goto('http://localhost:4000/tests/srb-en.html');

const searchBox = page.locator('#sch-inp-ac');
await expect(searchBox).toHaveAttribute('type', 'text');
await expect(searchBox).toHaveAttribute('role', 'combobox');
await expect(searchBox).toHaveAttribute('aria-expanded', 'false');
await expect(searchBox).toHaveAttribute('aria-autocomplete', 'list');
await expect(searchBox).toHaveAttribute('aria-controls', 'suggestions');

const suggestionsList = page.locator('form[role="search"] ul#suggestions');
await expect(suggestionsList).toHaveAttribute('role', 'listbox');
await expect(suggestionsList).toHaveClass(/query-suggestions/);
await expect(suggestionsList).toHaveAttribute('aria-describedby', 'sr-qs-hint');

const hint = page.locator('form[role="search"] p#sr-qs-hint');
await expect(hint).toHaveClass(/hidden/);
});

test('query suggestions appear and update while typing, then disappear when input is too short', async ({ page }) => {
const searchBox = page.locator('#sch-inp-ac');
const suggestionsList = page.locator('#suggestions');
const suggestionItems = suggestionsList.locator('li.suggestion-item');

await searchBox.focus();

// Type "canada" one character at a time, asserting suggestions only appear at 3+ characters.
for (const [i, char] of [...'canada'].entries()) {
// Wait for the suggestions API response before asserting UI state.
const responsePromise = page.waitForResponse(res => res.url().includes('/querySuggest'), { timeout: 5000 }).catch(() => null);
await page.keyboard.type(char);
await responsePromise;
const typedSoFar = 'canada'.slice(0, i + 1);

if (typedSoFar.length < 3) {
// Fewer than 3 characters — suggestions should not be shown.
await expect(searchBox).toHaveAttribute('aria-expanded', 'false');
await expect(suggestionsList).toHaveAttribute('hidden');

} else {
// 3 or more characters — wait for suggestions to load and verify count.
await expect(searchBox).toHaveAttribute('aria-expanded', 'true');
await expect(suggestionsList).not.toHaveAttribute('hidden');
await expect(suggestionItems.first()).toBeVisible();
const count = await suggestionItems.count();
expect(count, `expected 1–10 suggestions for "${typedSoFar}"`).toBeGreaterThanOrEqual(1);
expect(count, `expected 1–10 suggestions for "${typedSoFar}"`).toBeLessThanOrEqual(10);
}
}

// Backspace one character at a time. Suggestions should stay visible until input drops below 3 characters.
for (let remaining = 'canada'.length - 1; remaining >= 0; remaining--) {
const responsePromise = page.waitForResponse(res => res.url().includes('/querySuggest'), { timeout: 5000 }).catch(() => null);
await page.keyboard.press('Backspace');
await responsePromise;

if (remaining >= 3) {
await expect(searchBox).toHaveAttribute('aria-expanded', 'true');
await expect(suggestionsList).not.toHaveAttribute('hidden');
await expect(suggestionItems.first()).toBeVisible();
} else {
// Input is now 0–2 characters — suggestions should be hidden.
await expect(searchBox).toHaveAttribute('aria-expanded', 'false');
await expect(suggestionsList).toHaveAttribute('hidden');
}
}
});

test('clicking a query suggestion submits a search for that suggestion', async ({ page }) => {
const searchBox = page.locator('#sch-inp-ac');
const suggestionsList = page.locator('#suggestions');
const suggestionItems = suggestionsList.locator('li.suggestion-item');

await searchBox.focus();

// Wait for suggestions to load before clicking.
const responsePromise = page.waitForResponse(res => res.url().includes('/querySuggest'), { timeout: 5000 });
await page.keyboard.type('canada');
await responsePromise;

await expect(suggestionItems.first()).toBeVisible();
const secondSuggestion = suggestionItems.nth(1);
const suggestionText = await secondSuggestion.innerText();
await secondSuggestion.click();

// The suggestions box should close after clicking.
await expect(suggestionsList).toHaveAttribute('hidden');
await expect(searchBox).toHaveAttribute('aria-expanded', 'false');

// The search field should show the clicked suggestion's text.
await expect(searchBox).toHaveValue(suggestionText);

// Results should be returned for the selected suggestion.
const summary = page.locator('#wb-land h2');
await expect(summary).toBeVisible();
await expect(summary).toContainText(suggestionText);
});
});
76 changes: 76 additions & 0 deletions e2e/srb-en.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { test, expect } from '@playwright/test';

test.describe('SRB EN page', () => {
test.beforeEach(async ({ page }) => {
// Mask the automation flag that Playwright sets — Coveo detects it and skips initialization.
await page.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
});

await page.goto('http://localhost:4000/tests/srb-en.html');
});

test('search box and config are present', async ({ page }) => {
await expect(page.locator('#sch-inp-ac')).toBeVisible();

// connector.js reads the data-gc-search attribute to configure the Coveo search engine.
const configEl = page.locator('[data-gc-search]');
await expect(configEl).toBeAttached();
const attrValue = await configEl.getAttribute('data-gc-search');
expect(attrValue).toBeTruthy();
});

test('search library initializes successfully', async ({ page, context }) => {
// A query in the URL hash is required to trigger Coveo's first API call,
// which is what sets the tracking cookie and localStorage item.
await page.goto('http://localhost:4000/tests/srb-en.html#q=canada');

await page.waitForFunction(
() => document.cookie.includes('coveo_visitorId') || window.__coveoInitialized,
{ timeout: 5000 }
).catch(() => {});

// Fetch cookies via the browser context rather than JS so HttpOnly cookies are included.
const cookies = await context.cookies('http://localhost:4000');
const visitorCookie = cookies.find(c => c.name === 'coveo_visitorId');
expect(visitorCookie, 'coveo_visitorId cookie should exist').toBeTruthy();

await page.waitForFunction(() => localStorage.getItem('visitorId') !== null, { timeout: 5000 });
const visitorId = await page.evaluate(() => localStorage.getItem('visitorId'));
expect(visitorId, 'visitorId localStorage item should exist').toBeTruthy();
});

test('basic keyword search via keyboard submit', async ({ page }) => {
await page.locator('#sch-inp-ac').focus();
await page.keyboard.type('Canada');
await page.keyboard.press('Enter');

const summary = page.locator('#wb-land h2');
await expect(summary).toBeFocused();
await expect(summary).toBeVisible();
await expect(summary).toContainText('Canada');

// After keyboard interaction, the browser shows a visible focus ring (:focus-visible is true).
const hasFocusRing = await summary.evaluate(el => el.matches(':focus-visible'));
expect(hasFocusRing, 'focus ring should be visible after keyboard submit').toBe(true);

await expect(page.locator('#sch-inp-ac')).toHaveValue('Canada');
});

test('basic keyword search via mouse submit', async ({ page }) => {
await page.locator('#sch-inp-ac').click();
await page.keyboard.type('Canada');
await page.locator('form[role="search"] button[type="submit"]').click();

const summary = page.locator('#wb-land h2');
await expect(summary).toBeFocused();
await expect(summary).toBeVisible();
await expect(summary).toContainText('Canada');

// After mouse interaction, the browser suppresses the focus ring (:focus-visible is false).
const hasFocusRing = await summary.evaluate(el => el.matches(':focus-visible'));
expect(hasFocusRing, 'focus ring should not be visible after mouse submit').toBe(false);

await expect(page.locator('#sch-inp-ac')).toHaveValue('Canada');
});
});
Loading
Loading