Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

End-to-end tests #109

Open
wants to merge 4 commits into
base: v3
Choose a base branch
from
Open
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
27 changes: 27 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Playwright Tests

on:
- push
- pull_request

jobs:
test:
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@
/dist/*.mts
/dist/stats.json
/dist/example-assets/migrations.js
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
114 changes: 114 additions & 0 deletions e2e/specs/OrejimePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {expect, BrowserContext, Page} from '@playwright/test';
import Cookie from 'js-cookie';
import {Config} from '../../src/ui';

export class OrejimePage {
constructor(
public readonly page: Page,
public readonly context: BrowserContext
) {}

async load(config: Partial<Config>) {
await this.page.route('/', async (route) => {
await route.fulfill({
body: `
<!DOCTYPE html>

<html>
<head>
<title>Orejime</title>
<link rel="stylesheet" href="orejime.css" />
</head>

<body>
<script>
window.orejimeConfig = ${JSON.stringify(config)}
</script>
<script src="orejime.js"></script>
</body>
</html>
`
});
});

await this.page.goto('/');
}

get banner() {
return this.page.locator('.orejime-Banner');
}

get leanMoreBannerButton() {
return this.page.locator('.orejime-Banner-learnMoreButton');
}

get firstFocusableElementFromBanner() {
return this.page.locator('.orejime-Banner :is(a, button)').first();
}

get modal() {
return this.page.locator('.orejime-Modal');
}

purposeCheckbox(purposeId: string) {
return this.page.locator(`#orejime-purpose-${purposeId}`);
}

async focusNext() {
await this.page.keyboard.press('Tab');
}

async acceptAllFromBanner() {
await this.page.locator('.orejime-Banner-saveButton').click();
}

async declineAllFromBanner() {
await this.page.locator('.orejime-Banner-declineButton').click();
}

async openModalFromBanner() {
await this.leanMoreBannerButton.click();
}

async enableAllFromModal() {
await this.page.locator('.orejime-PurposeToggles-enableAll').click();
}

async disableAllFromModal() {
await this.page.locator('.orejime-PurposeToggles-disableAll').click();
}

async saveFromModal() {
await this.page.locator('.orejime-Modal-saveButton').click();
}

async closeModalByClickingButton() {
await this.page.locator('.orejime-Modal-closeButton').click();
}

async closeModalByClickingOutside() {
// We're clicking in a corner to avoid clicking on the
// modal itself, which has no effect.
await this.page.locator('.orejime-ModalOverlay').click({
position: {
x: 1,
y: 1
}
});
}

async closeModalByPressingEscape() {
await this.page.keyboard.press('Escape');
}

async expectConsents(consents: Record<string, unknown>) {
expect(await this.getConsentsFromCookies()).toEqual(consents);
}

async getConsentsFromCookies() {
const name = 'eu-consent';
const cookies = await this.context.cookies();
const {value} = cookies.find((cookie) => cookie.name === name)!;
return JSON.parse(Cookie.converter.read(value, name));
}
}
169 changes: 169 additions & 0 deletions e2e/specs/orejime.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import {test, expect} from '@playwright/test';
import {Config} from '../../src/ui';
import {OrejimePage} from './OrejimePage';

test.describe('Orejime', () => {
const BaseConfig: Partial<Config> = {
privacyPolicyUrl: 'https://example.org/privacy',
purposes: [
{
id: 'mandatory',
title: 'Mandatory',
cookies: ['mandatory'],
isMandatory: true
},
{
id: 'group',
title: 'Group',
purposes: [
{
id: 'child-1',
title: 'First child',
cookies: ['child-1']
},
{
id: 'child-2',
title: 'Second child',
cookies: ['child-2']
}
]
}
]
};

let orejimePage: OrejimePage;

test.beforeEach(async ({page, context}) => {
orejimePage = new OrejimePage(page, context);
await orejimePage.load(BaseConfig);
});

test('should show a banner', async () => {
await expect(orejimePage.banner).toBeVisible();
});

test('should navigate to the banner first', async () => {
await orejimePage.focusNext();
await expect(orejimePage.firstFocusableElementFromBanner).toBeFocused();
});

test('should accept all purposes from the banner', async () => {
await orejimePage.acceptAllFromBanner();
await expect(orejimePage.banner).not.toBeVisible();

orejimePage.expectConsents({
'mandatory': true,
'child-1': true,
'child-2': true
});
});

test('should decline all purposes from the banner', async () => {
await orejimePage.declineAllFromBanner();
await expect(orejimePage.banner).not.toBeVisible();

orejimePage.expectConsents({
'mandatory': true,
'child-1': false,
'child-2': false
});
});

test('should open a modal', async () => {
await orejimePage.openModalFromBanner();

await expect(orejimePage.banner).toBeVisible();
await expect(orejimePage.modal).toBeVisible();
});

test('should close the modal via the close button', async () => {
await orejimePage.openModalFromBanner();
await expect(orejimePage.modal).toBeVisible();

await orejimePage.closeModalByClickingButton();
await expect(orejimePage.modal).toHaveCount(0);
await expect(orejimePage.banner).toBeVisible();
});

test('should close the modal via the overlay', async () => {
await orejimePage.openModalFromBanner();
await expect(orejimePage.modal).toBeVisible();

await orejimePage.closeModalByClickingOutside();
await expect(orejimePage.modal).toHaveCount(0);
await expect(orejimePage.banner).toBeVisible();
});

test('should close the modal via `Escape` key', async () => {
await orejimePage.openModalFromBanner();
await expect(orejimePage.modal).toBeVisible();

await orejimePage.closeModalByPressingEscape();
await expect(orejimePage.modal).toHaveCount(0);
await expect(orejimePage.banner).toBeVisible();
});

test('should move focus after closing the modal', async () => {
await orejimePage.openModalFromBanner();
await expect(orejimePage.modal).toBeVisible();

await orejimePage.closeModalByPressingEscape();
await expect(orejimePage.leanMoreBannerButton).toBeFocused();
});

test('should accept all purposes from the modal', async () => {
await orejimePage.openModalFromBanner();
await orejimePage.enableAllFromModal();
await expect(orejimePage.purposeCheckbox('child-1')).toBeChecked();
await expect(orejimePage.purposeCheckbox('mandatory')).toBeChecked();
await orejimePage.saveFromModal();

orejimePage.expectConsents({
'mandatory': true,
'child-1': true,
'child-2': true
});
});

test('should decline all purposes from the modal', async () => {
await orejimePage.openModalFromBanner();
await orejimePage.enableAllFromModal();
await orejimePage.disableAllFromModal();
await expect(orejimePage.purposeCheckbox('child-1')).not.toBeChecked();
await expect(orejimePage.purposeCheckbox('mandatory')).toBeChecked();
await orejimePage.saveFromModal();

orejimePage.expectConsents({
'mandatory': true,
'child-1': false,
'child-2': false
});
});

test('should sync grouped purposes', async () => {
await orejimePage.openModalFromBanner();

const checkbox = orejimePage.purposeCheckbox('child-1');
await expect(checkbox).not.toBeChecked();

const checkbox2 = orejimePage.purposeCheckbox('child-2');
await expect(checkbox2).not.toBeChecked();

const groupCheckbox = orejimePage.purposeCheckbox('group');
await groupCheckbox.check();
await expect(groupCheckbox).toBeChecked();
await expect(checkbox).toBeChecked();
await expect(checkbox2).toBeChecked();

await checkbox.uncheck();
await expect(groupCheckbox).not.toBeChecked();
await expect(groupCheckbox).toHaveJSProperty('indeterminate', true);
await expect(checkbox).not.toBeChecked();
await expect(checkbox2).toBeChecked();

await checkbox2.uncheck();
await expect(groupCheckbox).not.toBeChecked();
await expect(checkbox).not.toBeChecked();
await expect(checkbox2).not.toBeChecked();
});
});
Loading