Skip to content

Commit

Permalink
Contextual consent
Browse files Browse the repository at this point in the history
@todo
* add translations
* handle focus with care
  • Loading branch information
felixgirault committed Feb 21, 2025
1 parent da7d0fd commit 0d5bbf5
Show file tree
Hide file tree
Showing 35 changed files with 679 additions and 103 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ purpose:
</template>
```

> [!NOTE] There is more you can do with templates! Learn about
> [contextual consent](#contextual-consent).
<details>
<summary>Integration tips</summary>

Expand Down Expand Up @@ -394,6 +397,55 @@ Romanian, Spanish, Swedish.
> [!NOTE] Each and every translated text is overridable via
> [the configuration](#configuration).
### Contextual consent

Content embedded from other websites might be restricted by user consent (i.e. a
YouTube video).

In that case, using templates would work just like with scripts:

```js
<template data-purpose="youtube">
<iframe src="https://www.youtube.com/embed/toto"></iframe>
</template>
```

However, this won't show anything until the user consents to the related
purpose.

To be a little more user friendly, adding the `data-contextual` attribute will
display a fallback notice until consent is given, detailing the reason and
offering a way to consent in place.

```diff
- <template data-purpose="youtube">
+ <template data-purpose="youtube" data-contextual>
<iframe src="https://www.youtube.com/embed/toto"></iframe>
</template>
```

<details>
<summary>Integration tips</summary>

#### WordPress

Should you use Orejime in a WordPress website, you could alter the rendering of
embeds so they use contextual consent:

```php
function orejimeWrapEmbeds($content, $block) {
if ($block['blockName'] === 'core/embed') {
return '<template data-purpose="embeds" data-contextual>' . $content . '</template>';
}

return $content;
}

add_filter('render_block', 'orejimeWrapEmbeds', 10, 2);
```

</details>

## API

Functions and references are made available on the global scope:
Expand Down
71 changes: 47 additions & 24 deletions e2e/OrejimePage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {expect, BrowserContext, Page} from '@playwright/test';
import {expect, BrowserContext, Page, Locator} from '@playwright/test';
import Cookie from 'js-cookie';
import {Config} from '../src/ui/types';

Expand All @@ -8,7 +8,7 @@ export class OrejimePage {
public readonly context: BrowserContext
) {}

async load(config: Partial<Config>, scripts: string) {
async load(config: Partial<Config>, body: string) {
await this.page.route('/', async (route) => {
await route.fulfill({
body: `
Expand All @@ -21,11 +21,21 @@ export class OrejimePage {
</head>
<body>
${body}
<!--
A dummy focusable element at the end of the page
so there is always something to reach when
navigating with the keyboard.
Otherwise, some tests would fail in Firefox.
@see https://github.com/microsoft/playwright/issues/32269
-->
<div tabindex="0"></div>
<script>
window.orejimeConfig = ${JSON.stringify(config)}
</script>
<script src="orejime-standard-en.js"></script>
${scripts}
</body>
</html>
`
Expand All @@ -36,61 +46,69 @@ export class OrejimePage {
}

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

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

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

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

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

get contextualNoticePlaceholder() {
return this.locator('.orejime-ContextualNotice-placeholder');
}

locator(selector: string) {
return this.page.locator(selector);
}

async focusNext() {
await this.page.keyboard.press('Tab');
purposeCheckbox(purposeId: string) {
return this.locator(`#orejime-purpose-${purposeId}`);
}

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

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

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

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

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

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

async closeModalByClickingButton() {
await this.page.locator('.orejime-Modal-closeButton').click();
await this.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({
await this.locator('.orejime-ModalOverlay').click({
position: {
x: 1,
y: 1
Expand All @@ -102,12 +120,8 @@ export class OrejimePage {
await this.page.keyboard.press('Escape');
}

async expectElement(selector: string) {
await expect(this.page.locator(selector)).toBeAttached();
}

async expectMissingElement(selector: string) {
await expect(this.page.locator(selector)).not.toBeAttached();
async acceptContextualNotice() {
await this.locator('.orejime-ContextualNotice-button').click();
}

async expectConsents(consents: Record<string, unknown>) {
Expand All @@ -120,4 +134,13 @@ export class OrejimePage {
const {value} = cookies.find((cookie) => cookie.name === name)!;
return JSON.parse(Cookie.converter.read(value, name));
}

// In specific conditions, browser events can get queued
// up and won't be fired until some interaction with the
// page.
// We're using a dummy click to trigger queued events.
// @see https://github.com/microsoft/playwright/issues/979
emptyEventQueue() {
return this.page.mouse.click(0, 0);
}
}
Loading

0 comments on commit 0d5bbf5

Please sign in to comment.