@playpilot/tpi 5.14.0 → 5.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/link-injections.js +10 -10
- package/package.json +1 -1
- package/src/lib/ads.ts +5 -0
- package/src/lib/consent.ts +13 -0
- package/src/lib/splitTest.ts +5 -0
- package/src/lib/tracking.ts +3 -0
- package/src/lib/types/consent.d.ts +9 -0
- package/src/lib/types/script.d.ts +3 -0
- package/src/main.ts +21 -1
- package/src/routes/+layout.svelte +20 -3
- package/src/routes/+page.svelte +13 -10
- package/src/routes/components/Consent.svelte +72 -0
- package/src/routes/components/Playlink.svelte +16 -4
- package/src/routes/components/TrackingPixels.svelte +7 -3
- package/src/tests/lib/ads.test.js +24 -1
- package/src/tests/lib/consent.test.js +50 -0
- package/src/tests/lib/splitTest.test.js +30 -6
- package/src/tests/lib/tracking.test.js +18 -3
- package/src/tests/routes/components/Consent.test.js +69 -0
- package/src/tests/routes/components/Playlink.test.js +22 -1
- package/src/tests/routes/components/TitleModal.test.js +6 -0
- package/src/tests/routes/components/TitlePopover.test.js +6 -0
- package/src/tests/routes/components/TrackingPixels.test.js +15 -1
- package/src/tests/setup.js +14 -1
package/package.json
CHANGED
package/src/lib/ads.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getApiToken } from "./api"
|
|
2
|
+
import { hasConsentedTo } from "./consent"
|
|
2
3
|
import { apiBaseUrl } from "./constants"
|
|
3
4
|
import { TrackingEvent } from "./enums/TrackingEvent"
|
|
4
5
|
import { track } from "./tracking"
|
|
@@ -6,6 +7,8 @@ import type { Campaign, CampaignFormat } from "./types/campaign"
|
|
|
6
7
|
import type { PlaylinkData } from "./types/playlink"
|
|
7
8
|
|
|
8
9
|
export async function fetchAds() {
|
|
10
|
+
if (!hasConsentedTo('ads')) return
|
|
11
|
+
|
|
9
12
|
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
10
13
|
const apiToken = getApiToken()
|
|
11
14
|
|
|
@@ -29,6 +32,8 @@ export async function fetchAds() {
|
|
|
29
32
|
* ad of the given format, which will likely always be the most recently added (or only) ad of that format.
|
|
30
33
|
*/
|
|
31
34
|
export function getFirstAdOfType(format: CampaignFormat): Campaign | null {
|
|
35
|
+
if (!hasConsentedTo('ads')) return null
|
|
36
|
+
|
|
32
37
|
return (window.PlayPilotLinkInjections?.ads || []).find(i => i.campaign_format === format) || null
|
|
33
38
|
}
|
|
34
39
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ConsentKeys, ConsentOptions } from "./types/consent"
|
|
2
|
+
|
|
3
|
+
export function setConsent(options: ConsentOptions): void {
|
|
4
|
+
if (!window.PlayPilotLinkInjections) return
|
|
5
|
+
|
|
6
|
+
window.PlayPilotLinkInjections.consents = options
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function hasConsentedTo(key: ConsentKeys): boolean {
|
|
10
|
+
if (window.PlayPilotLinkInjections.require_consent === false) return true
|
|
11
|
+
|
|
12
|
+
return !!window.PlayPilotLinkInjections.consents?.[key]
|
|
13
|
+
}
|
package/src/lib/splitTest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasConsentedTo } from "./consent"
|
|
1
2
|
import { TrackingEvent } from "./enums/TrackingEvent"
|
|
2
3
|
import { track } from "./tracking"
|
|
3
4
|
|
|
@@ -12,6 +13,8 @@ type SplitTest = {
|
|
|
12
13
|
* The identifier is saved on the window object so that it is consistent across this session.
|
|
13
14
|
*/
|
|
14
15
|
export function getSplitTestIdentifier({ key }: SplitTest): number {
|
|
16
|
+
if (!hasConsentedTo('tracking')) return 0
|
|
17
|
+
|
|
15
18
|
const windowIdentifier = window.PlayPilotLinkInjections?.split_test_identifiers?.[key]
|
|
16
19
|
if (windowIdentifier) return windowIdentifier
|
|
17
20
|
|
|
@@ -39,6 +42,8 @@ export function getSplitTestVariantName(test: SplitTest): string {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export function isInSplitTestVariant(test: SplitTest, variant = 1): boolean {
|
|
45
|
+
if (!hasConsentedTo('tracking')) return false
|
|
46
|
+
|
|
42
47
|
return getSplitTestVariantIndex(test) === variant
|
|
43
48
|
}
|
|
44
49
|
|
package/src/lib/tracking.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { hasConsentedTo } from "./consent"
|
|
1
2
|
import { mobileBreakpoint } from "./constants"
|
|
2
3
|
import type { TitleData } from "./types/title"
|
|
3
4
|
import { getFullUrlPath } from "./url"
|
|
@@ -12,6 +13,8 @@ const baseUrl = 'https://insights.playpilot.net'
|
|
|
12
13
|
* @param [payload] Any data that will be included with the event
|
|
13
14
|
*/
|
|
14
15
|
export async function track(event: string, title: TitleData | null = null, payload: Record<string, any> = {}): Promise<void> {
|
|
16
|
+
if (!hasConsentedTo('tracking')) return
|
|
17
|
+
|
|
15
18
|
const headers = new Headers({ 'Content-Type': 'application/json' })
|
|
16
19
|
|
|
17
20
|
if (title) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Campaign } from "./campaign"
|
|
2
|
+
import type { ConsentOptions } from "./consent"
|
|
2
3
|
import type { LinkInjection } from "./injection"
|
|
3
4
|
|
|
4
5
|
export type ScriptConfig = {
|
|
@@ -14,5 +15,7 @@ export type ScriptConfig = {
|
|
|
14
15
|
tracked_events?: { event: string, payload: Record<string, any> }[]
|
|
15
16
|
split_test_identifiers?: Record<string, number>
|
|
16
17
|
evaluated_link_injections?: LinkInjection[]
|
|
18
|
+
require_consent?: boolean
|
|
19
|
+
consents?: ConsentOptions
|
|
17
20
|
ads?: Campaign[]
|
|
18
21
|
}
|
package/src/main.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { mount } from 'svelte'
|
|
|
2
2
|
import App from './routes/+page.svelte'
|
|
3
3
|
import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/linkInjection'
|
|
4
4
|
import { getPageMetaData } from '$lib/meta'
|
|
5
|
+
import { setConsent } from '$lib/consent'
|
|
5
6
|
import type { Campaign } from '$lib/types/campaign'
|
|
6
7
|
|
|
7
8
|
window.PlayPilotLinkInjections = {
|
|
@@ -18,9 +19,17 @@ window.PlayPilotLinkInjections = {
|
|
|
18
19
|
split_test_identifiers: {},
|
|
19
20
|
evaluated_link_injections: [],
|
|
20
21
|
ads: [],
|
|
22
|
+
require_consent: true,
|
|
23
|
+
consents: {
|
|
24
|
+
ads: false,
|
|
25
|
+
pixels: false,
|
|
26
|
+
tracking: false,
|
|
27
|
+
split_tests: false,
|
|
28
|
+
affiliate: false,
|
|
29
|
+
},
|
|
21
30
|
app: null,
|
|
22
31
|
|
|
23
|
-
initialize(config = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '' }): void {
|
|
32
|
+
initialize(config = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, organization_sid: null, domain_sid: null, editorial_token: '', require_consent: true }): void {
|
|
24
33
|
if (!config.token) {
|
|
25
34
|
console.error('An API token is required.')
|
|
26
35
|
return
|
|
@@ -34,6 +43,17 @@ window.PlayPilotLinkInjections = {
|
|
|
34
43
|
this.language = config.language
|
|
35
44
|
this.organization_sid = config.organization_sid
|
|
36
45
|
this.domain_sid = config.domain_sid
|
|
46
|
+
this.require_consent = config.require_consent
|
|
47
|
+
|
|
48
|
+
if (!this.require_consent) {
|
|
49
|
+
setConsent({
|
|
50
|
+
ads: true,
|
|
51
|
+
pixels: true,
|
|
52
|
+
tracking: true,
|
|
53
|
+
split_tests: true,
|
|
54
|
+
affiliate: true,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
37
57
|
|
|
38
58
|
if (this.app) this.destroy()
|
|
39
59
|
|
|
@@ -8,9 +8,26 @@
|
|
|
8
8
|
|
|
9
9
|
const { children } = $props()
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
if (browser) {
|
|
12
|
+
// @ts-ignore
|
|
13
|
+
// This is normally given through window.PlayPilotLinkInjections.initialize({ token: 'some-token' })
|
|
14
|
+
window.PlayPilotLinkInjections = { token: 'ZoAL14yqzevMyQiwckbvyetOkeIUeEDN', selector: 'article', require_consent: true }
|
|
15
|
+
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
// Fake tcfapi consent to always be true in development. This way we can write the script assuming consent
|
|
18
|
+
// windows are standard and we don't have to make exceptions. If we do want actual exceptions they can be
|
|
19
|
+
// set via the config object using `require_consent`
|
|
20
|
+
window.__tcfapi = (command, _version, callback) => {
|
|
21
|
+
if (command !== 'addEventListener') return
|
|
22
|
+
|
|
23
|
+
setTimeout(() => callback({
|
|
24
|
+
purpose: { consents: { 1: true, 2: true, 7: true, 8: true, 9: true, 10: true } },
|
|
25
|
+
eventStatus: 'tcloaded',
|
|
26
|
+
}, true), 0)
|
|
27
|
+
|
|
28
|
+
return 1
|
|
29
|
+
}
|
|
30
|
+
}
|
|
14
31
|
</script>
|
|
15
32
|
|
|
16
33
|
<title>PlayPilot Link Injections</title>
|
package/src/routes/+page.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { onDestroy } from 'svelte'
|
|
3
3
|
import { fetchConfig, pollLinkInjections } from '$lib/api'
|
|
4
4
|
import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
|
|
5
5
|
import { setTrackingSids, track } from '$lib/tracking'
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
|
|
13
13
|
import Alert from './components/Editorial/Alert.svelte'
|
|
14
14
|
import TrackingPixels from './components/TrackingPixels.svelte'
|
|
15
|
+
import Consent from './components/Consent.svelte'
|
|
15
16
|
import Debugger from './components/Debugger.svelte'
|
|
16
17
|
import { fetchAds } from '$lib/ads'
|
|
17
18
|
|
|
@@ -36,18 +37,18 @@
|
|
|
36
37
|
if (isEditorialMode && !loading) rerender()
|
|
37
38
|
})
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
if (isCrawler()) return
|
|
40
|
+
onDestroy(clearLinkInjections)
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// This function is called when a user has properly consented via tcfapi or if no consent is required.
|
|
43
|
+
// Both of these options go through the Consent component.
|
|
44
|
+
async function start() {
|
|
45
|
+
if (isCrawler()) return
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
await initialize()
|
|
48
|
+
track(TrackingEvent.ArticlePageView)
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
}
|
|
50
|
+
if (aiInjections.length || manualInjections.length) window.PlayPilotLinkInjections.ads = await fetchAds()
|
|
51
|
+
}
|
|
51
52
|
|
|
52
53
|
async function initialize(): Promise<void> {
|
|
53
54
|
loading = true
|
|
@@ -205,6 +206,8 @@
|
|
|
205
206
|
<TrackingPixels pixels={response.pixels} />
|
|
206
207
|
{/if}
|
|
207
208
|
|
|
209
|
+
<Consent onchange={start} />
|
|
210
|
+
|
|
208
211
|
<style lang="scss">
|
|
209
212
|
@import url('$lib/scss/variables.scss');
|
|
210
213
|
@import url('$lib/scss/global.scss');
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setConsent } from '$lib/consent'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
onchange: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { onchange }: Props = $props()
|
|
9
|
+
|
|
10
|
+
const maxTries = 100
|
|
11
|
+
|
|
12
|
+
let currentTry = 1
|
|
13
|
+
|
|
14
|
+
listenForConsent()
|
|
15
|
+
|
|
16
|
+
function listenForConsent(): void {
|
|
17
|
+
// If require_consent has been explicitely turned off we return right away and call `onchange`.
|
|
18
|
+
// We don't need to set consent values as require_consent=false will mean all consent is true.
|
|
19
|
+
if (window.PlayPilotLinkInjections.require_consent === false) {
|
|
20
|
+
onchange()
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
if (!window.__tcfapi) {
|
|
26
|
+
// It could be that the consent script loads after our script. It's not terribly likely since our script is deferred,
|
|
27
|
+
// but still, it's possible. We retry for a little while to see if the script loads. If it doesn't load after x tries
|
|
28
|
+
// we assume there is no consent window at all.
|
|
29
|
+
if (currentTry < maxTries) setTimeout(listenForConsent, 100)
|
|
30
|
+
currentTry++
|
|
31
|
+
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
// '2' here is the version of tcfapi
|
|
37
|
+
window.__tcfapi('addEventListener', 2, (tcData, success) => {
|
|
38
|
+
if (!success) return
|
|
39
|
+
|
|
40
|
+
const { purpose, eventStatus } = tcData
|
|
41
|
+
|
|
42
|
+
if (eventStatus !== 'tcloaded' && eventStatus !== 'useractioncomplete') return
|
|
43
|
+
|
|
44
|
+
// Consent is given as a list of ids, each id giving consent for a certain type of data.
|
|
45
|
+
// We don't use most of these, but for future reference:
|
|
46
|
+
// 1 Store and/or access information on a device
|
|
47
|
+
// 2 Basic ads
|
|
48
|
+
// 3 Create a personalised ads profile
|
|
49
|
+
// 4 Select personalised ads
|
|
50
|
+
// 5 Create a personalised content profile
|
|
51
|
+
// 6 Select personalised content
|
|
52
|
+
// 7 Measure ad performance (impressions, clicks, conversions)
|
|
53
|
+
// 8 Measure content performance (engagement, viewability)
|
|
54
|
+
// 9 Apply market research to generate audience insights
|
|
55
|
+
// 10 Develop and improve products (Split tests, feature measurement)
|
|
56
|
+
// 11 Ensure security, prevent fraud, and debug (bot detection, fraud prevention)
|
|
57
|
+
// When there's overlap or ambiguity we select all possibly relevant categories
|
|
58
|
+
|
|
59
|
+
const consent = (id: number) => purpose.consents[id] === true
|
|
60
|
+
|
|
61
|
+
setConsent({
|
|
62
|
+
ads: consent(1),
|
|
63
|
+
pixels: consent(1) && consent(7),
|
|
64
|
+
split_tests: consent(9) && consent(10),
|
|
65
|
+
tracking: consent(7) && consent(8),
|
|
66
|
+
affiliate: consent(1) && consent(7),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
onchange()
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Disclaimer from './Ads/Disclaimer.svelte'
|
|
3
|
+
import { hasConsentedTo } from '$lib/consent'
|
|
3
4
|
import { removeImageUrlPrefix } from '$lib/image'
|
|
4
5
|
import { t } from '$lib/localization'
|
|
5
6
|
import type { PlaylinkData } from '$lib/types/playlink'
|
|
@@ -23,7 +24,18 @@
|
|
|
23
24
|
}
|
|
24
25
|
</script>
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
28
|
+
<svelte:element
|
|
29
|
+
{onclick}
|
|
30
|
+
this={hasConsentedTo('affiliate') ? 'a' : 'span'}
|
|
31
|
+
href={url}
|
|
32
|
+
target="_blank"
|
|
33
|
+
class="playlink"
|
|
34
|
+
class:highlighted={highlighted && cta_text}
|
|
35
|
+
class:no-category={hideCategory}
|
|
36
|
+
data-playlink={name}
|
|
37
|
+
rel="sponsored">
|
|
38
|
+
|
|
27
39
|
<div class="playlink-content">
|
|
28
40
|
<img src={removeImageUrlPrefix(logo_url)} alt="" height="36" width="36" />
|
|
29
41
|
|
|
@@ -45,7 +57,7 @@
|
|
|
45
57
|
<Disclaimer {disclaimer} small />
|
|
46
58
|
{/if}
|
|
47
59
|
</div>
|
|
48
|
-
</
|
|
60
|
+
</svelte:element>
|
|
49
61
|
|
|
50
62
|
<style lang="scss">
|
|
51
63
|
$image-size: margin(2.25);
|
|
@@ -81,8 +93,8 @@
|
|
|
81
93
|
font-size: var(--playpilot-playlinks-font-size, margin(0.75));
|
|
82
94
|
line-height: 1;
|
|
83
95
|
|
|
84
|
-
&:hover,
|
|
85
|
-
&:active {
|
|
96
|
+
&:not(span):hover,
|
|
97
|
+
&:not(span):active {
|
|
86
98
|
filter: var(--playpilot-playlink-hover-filter, brightness(1.1));
|
|
87
99
|
background: var(--playpilot-playlink-hover-background, var(--playpilot-playlink-background, var(--playpilot-lighter))) !important;
|
|
88
100
|
text-decoration: none !important;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { hasConsentedTo } from '$lib/consent'
|
|
3
|
+
|
|
2
4
|
interface Props {
|
|
3
5
|
pixels: string[]
|
|
4
6
|
}
|
|
@@ -6,9 +8,11 @@
|
|
|
6
8
|
const { pixels }: Props = $props()
|
|
7
9
|
</script>
|
|
8
10
|
|
|
9
|
-
{#
|
|
10
|
-
|
|
11
|
-
{
|
|
11
|
+
{#if hasConsentedTo('pixels')}
|
|
12
|
+
{#each pixels as src}
|
|
13
|
+
<img {src} alt="" />
|
|
14
|
+
{/each}
|
|
15
|
+
{/if}
|
|
12
16
|
|
|
13
17
|
<style lang="scss">
|
|
14
18
|
img {
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'
|
|
|
2
2
|
import { fakeFetch } from '../helpers'
|
|
3
3
|
|
|
4
4
|
import { fetchAds, getFirstAdOfType } from '$lib/ads'
|
|
5
|
+
import { hasConsentedTo } from '$lib/consent'
|
|
5
6
|
import { track } from '$lib/tracking'
|
|
6
7
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
7
8
|
|
|
@@ -9,11 +10,17 @@ vi.mock('$lib/tracking', () => ({
|
|
|
9
10
|
track: vi.fn(),
|
|
10
11
|
}))
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
vi.mock('$lib/consent', () => ({
|
|
14
|
+
hasConsentedTo: vi.fn(() => true),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
describe('$lib/ads', () => {
|
|
13
18
|
afterEach(() => {
|
|
14
19
|
vi.resetAllMocks()
|
|
15
20
|
// @ts-ignore
|
|
16
21
|
window.PlayPilotLinkInjections = null
|
|
22
|
+
|
|
23
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => true)
|
|
17
24
|
})
|
|
18
25
|
|
|
19
26
|
describe('fetchAds', () => {
|
|
@@ -42,6 +49,13 @@ describe('$lib/api', () => {
|
|
|
42
49
|
expect(global.fetch).not.toHaveBeenCalled()
|
|
43
50
|
})
|
|
44
51
|
|
|
52
|
+
it('Should not call fetch if user did not consent to ads', async () => {
|
|
53
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|
|
54
|
+
|
|
55
|
+
await fetchAds()
|
|
56
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
57
|
+
})
|
|
58
|
+
|
|
45
59
|
it('Should fire track event when ads failed to fetch', async () => {
|
|
46
60
|
fakeFetch({ ok: false, status: 505 })
|
|
47
61
|
|
|
@@ -64,5 +78,14 @@ describe('$lib/api', () => {
|
|
|
64
78
|
|
|
65
79
|
expect(getFirstAdOfType('top_scroll')).toBe(null)
|
|
66
80
|
})
|
|
81
|
+
|
|
82
|
+
it('Should not return ads if valid ad was given but user did not consent to ads', async () => {
|
|
83
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|
|
84
|
+
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
window.PlayPilotLinkInjections = { ads: [{ campaign_format: 'top_scroll' }] }
|
|
87
|
+
|
|
88
|
+
expect(getFirstAdOfType('top_scroll')).toBe(null)
|
|
89
|
+
})
|
|
67
90
|
})
|
|
68
91
|
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { setConsent, hasConsentedTo } from '$lib/consent'
|
|
4
|
+
|
|
5
|
+
describe('$lib/consent', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
window.PlayPilotLinkInjections = null
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('setConsent', () => {
|
|
12
|
+
it('Should set consent in window object to given values', async () => {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
window.PlayPilotLinkInjections = {}
|
|
15
|
+
|
|
16
|
+
setConsent({ ads: true, affiliate: true, pixels: true, split_tests: false, tracking: false })
|
|
17
|
+
|
|
18
|
+
expect(window.PlayPilotLinkInjections.consents).toEqual({ ads: true, affiliate: true, pixels: true, split_tests: false, tracking: false })
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('hasConsentedTo', () => {
|
|
23
|
+
it('Should return values for given keys from window object', async () => {
|
|
24
|
+
// @ts-ignore
|
|
25
|
+
window.PlayPilotLinkInjections = {
|
|
26
|
+
consents: { ads: true, affiliate: true, pixels: true, split_tests: false, tracking: false },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
expect(hasConsentedTo('ads')).toBe(true)
|
|
30
|
+
expect(hasConsentedTo('affiliate')).toBe(true)
|
|
31
|
+
expect(hasConsentedTo('pixels')).toBe(true)
|
|
32
|
+
expect(hasConsentedTo('split_tests')).toBe(false)
|
|
33
|
+
expect(hasConsentedTo('tracking')).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('Should return true regardless of values if require_consent is false', async () => {
|
|
37
|
+
// @ts-ignore
|
|
38
|
+
window.PlayPilotLinkInjections = {
|
|
39
|
+
require_consent: false,
|
|
40
|
+
consents: { ads: true, affiliate: true, pixels: true, split_tests: false, tracking: false },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
expect(hasConsentedTo('ads')).toBe(true)
|
|
44
|
+
expect(hasConsentedTo('affiliate')).toBe(true)
|
|
45
|
+
expect(hasConsentedTo('pixels')).toBe(true)
|
|
46
|
+
expect(hasConsentedTo('split_tests')).toBe(true)
|
|
47
|
+
expect(hasConsentedTo('tracking')).toBe(true)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -2,6 +2,11 @@ import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
|
2
2
|
import { getSplitTestIdentifier, getSplitTestVariantIndex, getSplitTestVariantName, isInSplitTestVariant, trackSplitTestAction, trackSplitTestView } from '$lib/splitTest'
|
|
3
3
|
import { track } from '$lib/tracking'
|
|
4
4
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
5
|
+
import { hasConsentedTo } from '$lib/consent'
|
|
6
|
+
|
|
7
|
+
vi.mock('$lib/consent', () => ({
|
|
8
|
+
hasConsentedTo: vi.fn(() => true),
|
|
9
|
+
}))
|
|
5
10
|
|
|
6
11
|
vi.mock('$lib/tracking', () => ({
|
|
7
12
|
track: vi.fn(),
|
|
@@ -13,13 +18,16 @@ describe('$lib/splitTest', () => {
|
|
|
13
18
|
numberOfVariants: 2,
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
window.PlayPilotLinkInjections = {}
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
window.PlayPilotLinkInjections = {}
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
25
|
+
vi.resetAllMocks()
|
|
26
|
+
|
|
27
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => true)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('getSplitTestIdentifier', () => {
|
|
23
31
|
|
|
24
32
|
it('Should set window object with given key if window object was not set to begin with', () => {
|
|
25
33
|
// @ts-ignore
|
|
@@ -52,6 +60,12 @@ describe('$lib/splitTest', () => {
|
|
|
52
60
|
'Some other key': expect.any(Number),
|
|
53
61
|
})
|
|
54
62
|
})
|
|
63
|
+
|
|
64
|
+
it('Should return 0 if user did not consent', () => {
|
|
65
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|
|
66
|
+
|
|
67
|
+
expect(getSplitTestIdentifier(splitTest)).toBe(0)
|
|
68
|
+
})
|
|
55
69
|
})
|
|
56
70
|
|
|
57
71
|
describe('isInSplitTestVariant', () => {
|
|
@@ -93,6 +107,16 @@ describe('$lib/splitTest', () => {
|
|
|
93
107
|
expect(isInSplitTestVariant(multiple, 1)).toBe(false)
|
|
94
108
|
expect(isInSplitTestVariant(multiple, 2)).toBe(true)
|
|
95
109
|
})
|
|
110
|
+
|
|
111
|
+
it('Should return false if user did not consent', () => {
|
|
112
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|
|
113
|
+
|
|
114
|
+
// @ts-ignore
|
|
115
|
+
window.PlayPilotLinkInjections.split_test_identifiers = {}
|
|
116
|
+
window.PlayPilotLinkInjections.split_test_identifiers[splitTest.key] = 0.75
|
|
117
|
+
|
|
118
|
+
expect(isInSplitTestVariant(splitTest)).toBe(false)
|
|
119
|
+
})
|
|
96
120
|
})
|
|
97
121
|
|
|
98
122
|
describe('trackSplitTestView', () => {
|
|
@@ -3,8 +3,12 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
3
3
|
import { setTrackingSids, track } from '$lib/tracking'
|
|
4
4
|
import { title } from '$lib/fakeData'
|
|
5
5
|
import { getFullUrlPath } from '$lib/url'
|
|
6
|
+
import { fakeFetch } from '../helpers'
|
|
7
|
+
import { hasConsentedTo } from '$lib/consent'
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
vi.mock('$lib/consent', () => ({
|
|
10
|
+
hasConsentedTo: vi.fn(() => true),
|
|
11
|
+
}))
|
|
8
12
|
|
|
9
13
|
describe('$lib/tracking', () => {
|
|
10
14
|
beforeEach(() => {
|
|
@@ -12,6 +16,9 @@ describe('$lib/tracking', () => {
|
|
|
12
16
|
window.PlayPilotLinkInjections = {}
|
|
13
17
|
|
|
14
18
|
vi.resetAllMocks()
|
|
19
|
+
|
|
20
|
+
fakeFetch()
|
|
21
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => true)
|
|
15
22
|
})
|
|
16
23
|
|
|
17
24
|
describe('track', () => {
|
|
@@ -149,6 +156,16 @@ describe('$lib/tracking', () => {
|
|
|
149
156
|
)
|
|
150
157
|
})
|
|
151
158
|
|
|
159
|
+
it('Should not track if user did not consent to tracking', () => {
|
|
160
|
+
vi.mocked(hasConsentedTo).mockImplementation(() => false)
|
|
161
|
+
|
|
162
|
+
track('Some event')
|
|
163
|
+
|
|
164
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('setTrackingSids', () => {
|
|
152
169
|
it('Should include device information', () => {
|
|
153
170
|
track('Some event')
|
|
154
171
|
|
|
@@ -159,9 +176,7 @@ describe('$lib/tracking', () => {
|
|
|
159
176
|
}),
|
|
160
177
|
)
|
|
161
178
|
})
|
|
162
|
-
})
|
|
163
179
|
|
|
164
|
-
describe('setTrackingSids', () => {
|
|
165
180
|
it('Should set stores equal to the given values', () => {
|
|
166
181
|
setTrackingSids({ domainSid: 'some-domain', organizationSid: 'some-organization' })
|
|
167
182
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import Consent from '../../../routes/components/Consent.svelte'
|
|
5
|
+
|
|
6
|
+
describe('Consent.svelte', () => {
|
|
7
|
+
it('Should call onchange right away when require_consent is false', () => {
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
window.PlayPilotLinkInjections = { require_consent: false }
|
|
10
|
+
|
|
11
|
+
const onchange = vi.fn()
|
|
12
|
+
|
|
13
|
+
render(Consent, { onchange })
|
|
14
|
+
|
|
15
|
+
expect(onchange).toHaveBeenCalled()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('Should call onchange after user has consented', () => {
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
window.PlayPilotLinkInjections = { require_consent: true }
|
|
21
|
+
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
window.__tcfapi = (command, _version, callback) => {
|
|
24
|
+
if (command !== 'addEventListener') return
|
|
25
|
+
|
|
26
|
+
callback({
|
|
27
|
+
purpose: { consents: { 1: true, 2: true, 7: true, 8: true, 9: true, 10: true } },
|
|
28
|
+
eventStatus: 'tcloaded',
|
|
29
|
+
}, true)
|
|
30
|
+
|
|
31
|
+
return 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const onchange = vi.fn()
|
|
35
|
+
|
|
36
|
+
render(Consent, { onchange })
|
|
37
|
+
|
|
38
|
+
// @ts-ignore
|
|
39
|
+
window.__tcfapi()
|
|
40
|
+
|
|
41
|
+
expect(onchange).toHaveBeenCalled()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('Should not call onchange after user has consented but eventStatus is invalid', () => {
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
window.PlayPilotLinkInjections = { require_consent: true }
|
|
47
|
+
|
|
48
|
+
// @ts-ignore
|
|
49
|
+
window.__tcfapi = (command, _version, callback) => {
|
|
50
|
+
if (command !== 'addEventListener') return
|
|
51
|
+
|
|
52
|
+
callback({
|
|
53
|
+
purpose: { consents: { 1: true, 2: true, 7: true, 8: true, 9: true, 10: true } },
|
|
54
|
+
eventStatus: 'invalid',
|
|
55
|
+
}, true)
|
|
56
|
+
|
|
57
|
+
return 1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const onchange = vi.fn()
|
|
61
|
+
|
|
62
|
+
render(Consent, { onchange })
|
|
63
|
+
|
|
64
|
+
// @ts-ignore
|
|
65
|
+
window.__tcfapi()
|
|
66
|
+
|
|
67
|
+
expect(onchange).not.toHaveBeenCalled()
|
|
68
|
+
})
|
|
69
|
+
})
|