Skip to content

Commit 0d5bbf5

Browse files
committed
Contextual consent
@todo * add translations * handle focus with care
1 parent da7d0fd commit 0d5bbf5

35 files changed

Lines changed: 679 additions & 103 deletions

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,9 @@ purpose:
303303
</template>
304304
```
305305

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

@@ -394,6 +397,55 @@ Romanian, Spanish, Swedish.
394397
> [!NOTE] Each and every translated text is overridable via
395398
> [the configuration](#configuration).
396399
400+
### Contextual consent
401+
402+
Content embedded from other websites might be restricted by user consent (i.e. a
403+
YouTube video).
404+
405+
In that case, using templates would work just like with scripts:
406+
407+
```js
408+
<template data-purpose="youtube">
409+
<iframe src="https://www.youtube.com/embed/toto"></iframe>
410+
</template>
411+
```
412+
413+
However, this won't show anything until the user consents to the related
414+
purpose.
415+
416+
To be a little more user friendly, adding the `data-contextual` attribute will
417+
display a fallback notice until consent is given, detailing the reason and
418+
offering a way to consent in place.
419+
420+
```diff
421+
- <template data-purpose="youtube">
422+
+ <template data-purpose="youtube" data-contextual>
423+
<iframe src="https://www.youtube.com/embed/toto"></iframe>
424+
</template>
425+
```
426+
427+
<details>
428+
<summary>Integration tips</summary>
429+
430+
#### WordPress
431+
432+
Should you use Orejime in a WordPress website, you could alter the rendering of
433+
embeds so they use contextual consent:
434+
435+
```php
436+
function orejimeWrapEmbeds($content, $block) {
437+
if ($block['blockName'] === 'core/embed') {
438+
return '<template data-purpose="embeds" data-contextual>' . $content . '</template>';
439+
}
440+
441+
return $content;
442+
}
443+
444+
add_filter('render_block', 'orejimeWrapEmbeds', 10, 2);
445+
```
446+
447+
</details>
448+
397449
## API
398450

399451
Functions and references are made available on the global scope:

e2e/OrejimePage.ts

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {expect, BrowserContext, Page} from '@playwright/test';
1+
import {expect, BrowserContext, Page, Locator} from '@playwright/test';
22
import Cookie from 'js-cookie';
33
import {Config} from '../src/ui/types';
44

@@ -8,7 +8,7 @@ export class OrejimePage {
88
public readonly context: BrowserContext
99
) {}
1010

11-
async load(config: Partial<Config>, scripts: string) {
11+
async load(config: Partial<Config>, body: string) {
1212
await this.page.route('/', async (route) => {
1313
await route.fulfill({
1414
body: `
@@ -21,11 +21,21 @@ export class OrejimePage {
2121
</head>
2222
2323
<body>
24+
${body}
25+
26+
<!--
27+
A dummy focusable element at the end of the page
28+
so there is always something to reach when
29+
navigating with the keyboard.
30+
Otherwise, some tests would fail in Firefox.
31+
@see https://github.com/microsoft/playwright/issues/32269
32+
-->
33+
<div tabindex="0"></div>
34+
2435
<script>
2536
window.orejimeConfig = ${JSON.stringify(config)}
2637
</script>
2738
<script src="orejime-standard-en.js"></script>
28-
${scripts}
2939
</body>
3040
</html>
3141
`
@@ -36,61 +46,69 @@ export class OrejimePage {
3646
}
3747

3848
get banner() {
39-
return this.page.locator('.orejime-Banner');
49+
return this.locator('.orejime-Banner');
4050
}
4151

4252
get learnMoreBannerButton() {
43-
return this.page.locator('.orejime-Banner-learnMoreButton');
53+
return this.locator('.orejime-Banner-learnMoreButton');
4454
}
4555

4656
get firstFocusableElementFromBanner() {
47-
return this.page.locator('.orejime-Banner :is(a, button)').first();
57+
return this.locator('.orejime-Banner :is(a, button)').first();
4858
}
4959

5060
get modal() {
51-
return this.page.locator('.orejime-Modal');
61+
return this.locator('.orejime-Modal');
5262
}
5363

54-
purposeCheckbox(purposeId: string) {
55-
return this.page.locator(`#orejime-purpose-${purposeId}`);
64+
get contextualNotice() {
65+
return this.locator('.orejime-ContextualNotice');
66+
}
67+
68+
get contextualNoticePlaceholder() {
69+
return this.locator('.orejime-ContextualNotice-placeholder');
70+
}
71+
72+
locator(selector: string) {
73+
return this.page.locator(selector);
5674
}
5775

58-
async focusNext() {
59-
await this.page.keyboard.press('Tab');
76+
purposeCheckbox(purposeId: string) {
77+
return this.locator(`#orejime-purpose-${purposeId}`);
6078
}
6179

6280
async acceptAllFromBanner() {
63-
await this.page.locator('.orejime-Banner-saveButton').click();
81+
await this.locator('.orejime-Banner-saveButton').click();
6482
}
6583

6684
async declineAllFromBanner() {
67-
await this.page.locator('.orejime-Banner-declineButton').click();
85+
await this.locator('.orejime-Banner-declineButton').click();
6886
}
6987

7088
async openModalFromBanner() {
7189
await this.learnMoreBannerButton.click();
7290
}
7391

7492
async enableAllFromModal() {
75-
await this.page.locator('.orejime-PurposeToggles-enableAll').click();
93+
await this.locator('.orejime-PurposeToggles-enableAll').click();
7694
}
7795

7896
async disableAllFromModal() {
79-
await this.page.locator('.orejime-PurposeToggles-disableAll').click();
97+
await this.locator('.orejime-PurposeToggles-disableAll').click();
8098
}
8199

82100
async saveFromModal() {
83-
await this.page.locator('.orejime-Modal-saveButton').click();
101+
await this.locator('.orejime-Modal-saveButton').click();
84102
}
85103

86104
async closeModalByClickingButton() {
87-
await this.page.locator('.orejime-Modal-closeButton').click();
105+
await this.locator('.orejime-Modal-closeButton').click();
88106
}
89107

90108
async closeModalByClickingOutside() {
91109
// We're clicking in a corner to avoid clicking on the
92110
// modal itself, which has no effect.
93-
await this.page.locator('.orejime-ModalOverlay').click({
111+
await this.locator('.orejime-ModalOverlay').click({
94112
position: {
95113
x: 1,
96114
y: 1
@@ -102,12 +120,8 @@ export class OrejimePage {
102120
await this.page.keyboard.press('Escape');
103121
}
104122

105-
async expectElement(selector: string) {
106-
await expect(this.page.locator(selector)).toBeAttached();
107-
}
108-
109-
async expectMissingElement(selector: string) {
110-
await expect(this.page.locator(selector)).not.toBeAttached();
123+
async acceptContextualNotice() {
124+
await this.locator('.orejime-ContextualNotice-button').click();
111125
}
112126

113127
async expectConsents(consents: Record<string, unknown>) {
@@ -120,4 +134,13 @@ export class OrejimePage {
120134
const {value} = cookies.find((cookie) => cookie.name === name)!;
121135
return JSON.parse(Cookie.converter.read(value, name));
122136
}
137+
138+
// In specific conditions, browser events can get queued
139+
// up and won't be fired until some interaction with the
140+
// page.
141+
// We're using a dummy click to trigger queued events.
142+
// @see https://github.com/microsoft/playwright/issues/979
143+
emptyEventQueue() {
144+
return this.page.mouse.click(0, 0);
145+
}
123146
}

0 commit comments

Comments
 (0)