Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
10 changes: 9 additions & 1 deletion packages/consent/consent-tools-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@
"lint": "yarn concurrently 'yarn:eslint .' 'yarn:tsc --noEmit'",
Copy link
Contributor

@silesky silesky Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Can we remove all the old wdio dependencies?
  2. Can we update/commit the yarn.lock? (CI is failing)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor Author

@neelkanth-kaushik neelkanth-kaushik Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is still failing after pushing yarn.lock. I will look into it separately once all the test cases have been migrated.

"eslint": "yarn run -T eslint",
"tsc": "tsc",
"concurrently": "yarn run -T concurrently --raw"
"concurrently": "yarn run -T concurrently --raw",
"playwright-test": "playwright test",
"playwright-test:headed": "playwright test --headed",
"playwright-test:debug": "playwright test --debug",
"serve": "http-server public -p 4567",
"server": "http-server --port 5432",
Copy link
Contributor

@silesky silesky Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why have serve and server? can we just use one -- I prefer "serve" just because that's the name we use in signals-integration-tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I forgot to remove the server. Thanks for pointing that out.

"browser": "playwright test --debug"
},
"installConfig": {
"hoistingLimits": "workspaces"
},
"devDependencies": {
"@internal/test-helpers": "workspace:^",
"@playwright/test": "^1.28.1",
"@segment/analytics-consent-tools": "workspace:^",
"@segment/analytics-consent-wrapper-onetrust": "workspace:^",
"@segment/analytics-next": "workspace:^",
Expand All @@ -29,6 +36,7 @@
"@wdio/types": "8",
"expect": "^29.4.1",
"globby": "^11.0.2",
"http-server": "14.1.1",
"wdio-intercept-service": "^4.4.0",
"webpack": "^5.94.0",
"webpack-cli": "^4.8.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { PlaywrightTestConfig } from '@playwright/test'
import { devices } from '@playwright/test'
import path from 'path'

/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
webServer: {
command: 'yarn run server',
url: 'http://127.0.0.1:5432',
reuseExistingServer: !process.env.CI,
},
testDir: './src/tests',
globalSetup: path.resolve(__dirname, 'playwright.global-setup.ts'),
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [['html', { open: 'never' }]],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
baseURL: `http://127.0.0.1:5432`,
trace: 'on',
launchOptions: {
args: ['--enable-precise-memory-info', '--js-flags=--expose-gc'],
},
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
],
}

export default config
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { FullConfig } from '@playwright/test'
import { execSync } from 'child_process'

export default function globalSetup(_cfg: FullConfig) {
console.log('Executing global setup...')
execSync('yarn build', { stdio: 'inherit' })
console.log('Finished global setup.')
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<h1>Hello World - Serving Analytics</h1>
<h2>Please Check Network tab</h2>
<p>This page can used as playground or run by webdriver.io</p>
<script src="/public/dist/consent-tools-vanilla-opt-in.bundle.js"></script>
<script src="dist/consent-tools-vanilla-opt-in.bundle.js"></script>
</body>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<h1>Hello World - Serving Analytics (Consent Tools Vanilla Opt Out)</h1>
<h2>Please Check Network tab</h2>
<p>This page can used as playground or run by webdriver.io</p>
<script src="/public/dist/consent-tools-vanilla-opt-out.bundle.js"></script>
<script src="dist/consent-tools-vanilla-opt-out.bundle.js"></script>
</body>

</html>
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const analytics = new AnalyticsBrowser()

withMockCMP(analytics).load(
{
writeKey: '9lSrez3BlfLAJ7NOChrqWtILiATiycoc',
writeKey: 'foo',
},
{ initialPageview: true }
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,124 +1,115 @@
import { CDNSettingsBuilder } from '@internal/test-helpers'
import type { SegmentEvent } from '@segment/analytics-next'
import assert from 'assert'
import type { Matches } from 'webdriverio'

const waitUntilReady = () =>
browser.waitUntil(
() => browser.execute(() => document.readyState === 'complete'),
{
timeout: 10000,
}
)
import { Page, Route, Request } from '@playwright/test'

export abstract class BasePage {
constructor(protected page: string) {}
protected page: Page
protected pageFile: string

segmentTrackingApiReqs: any[] = []
fetchIntegrationReqs: any[] = []

segmentTrackingApiReqs: Matches[] = []
fetchIntegrationReqs: Matches[] = []
constructor(page: Page, pageFile: string) {
this.page = page
this.pageFile = pageFile
}

async load(): Promise<void> {
const baseURL = browser.options.baseUrl
assert(baseURL)
await this.mockAPIs()
await browser.url(baseURL + '/public/' + this.page)
await waitUntilReady()
await this.page.goto(`/${this.pageFile}`)
await this.page.waitForLoadState('load')
}

async clearStorage() {
await browser.deleteAllCookies()
await browser.execute(() => window.localStorage.clear())
await this.page.context().clearCookies()
await this.page.evaluate(() => {
window.localStorage.clear()
window.sessionStorage.clear()
})
}

getAllTrackingEvents(): SegmentEvent[] {
const reqBodies = this.segmentTrackingApiReqs.map((el) =>
JSON.parse(el.postData!)
)
return reqBodies
async cleanup() {
this.segmentTrackingApiReqs = []
this.fetchIntegrationReqs = []
await this.clearStorage()
}

getAllTrackingEvents(): any[] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use SegmentEvent[] rather than any, like we were before? Ditto anywhere else 'any' is used

Copy link
Contributor Author

@neelkanth-kaushik neelkanth-kaushik Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated segmentTrackingApiReqs: SegmentEvent[] = [] but had to use fetchIntegrationReqs: CoreExtraContext[] = [] because of a linter error in the private async mockNextIntegrationsAPI() {...} at the line this.fetchIntegrationReqs.push({ url: request.url() }). The Error says: 'url' does not exist in type 'SegmentEvent'.

The only interface where url is available is [CoreExtraContext](https://github.com/segmentio/analytics-next/blob/master/packages/core/src/events/interfaces.ts). Being the only interface fitting in this scenario I assume it is fine to use it. What do you think?

return this.segmentTrackingApiReqs
}

getConsentChangedEvents(): SegmentEvent[] {
const reqBodies = this.getAllTrackingEvents()
const consentEvents = reqBodies.filter(
getConsentChangedEvents(): any[] {
return this.getAllTrackingEvents().filter(
(el) => el.event === 'Segment Consent Preference Updated'
)
return consentEvents
}

async cleanup() {
this.segmentTrackingApiReqs = []
this.fetchIntegrationReqs = []
await this.clearStorage()
get fetchIntegrationReqsData(): any[] {
return this.fetchIntegrationReqs
}

async mockAPIs() {
private async mockAPIs() {
await this.mockSegmentTrackingAPI()
await this.mockCDNSettingsAPI()
await this.mockNextIntegrationsAPI()
}

private async mockSegmentTrackingAPI(): Promise<void> {
const mock = await browser.mock('https://api.segment.io/v1/t', {
method: 'post',
})
mock.respond((mock) => {
this.segmentTrackingApiReqs.push(mock)
// response with status 200
return Promise.resolve({
statusCode: 200,
body: JSON.stringify({ success: true }),
})
})
private async mockSegmentTrackingAPI() {
await this.page.route(
'https://api.segment.io/v1/t',
async (route: Route, request: Request) => {
const postData = await request.postData()
const parsed = JSON.parse(postData || '{}')
this.segmentTrackingApiReqs.push(parsed) // store the parsed event object directly

await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ success: true }),
})
}
)
}

private async mockNextIntegrationsAPI(): Promise<void> {
const mock = await browser.mock('**/next-integrations/**')
mock.respond((mock) => {
this.fetchIntegrationReqs.push(mock)
return Promise.resolve({
statusCode: 200,
body: 'console.log("mocking action and classic destinations")',
})
})
private async mockNextIntegrationsAPI() {
await this.page.route(
'**/next-integrations/**',
async (route: Route, request: Request) => {
this.fetchIntegrationReqs.push({ url: request.url() })
await route.fulfill({
status: 200,
body: 'console.log("mocking action and classic destinations")',
contentType: 'application/javascript',
})
}
)
}

/** * Mock the CDN Settings endpoint so that this can run offline
*/
private async mockCDNSettingsAPI(): Promise<void> {
const settings = new CDNSettingsBuilder({
writeKey: 'something',
})
.addActionDestinationSettings(
{
private async mockCDNSettingsAPI() {
Copy link
Contributor

@silesky silesky Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we still use CDNSettingsBuilder? The point is that it also updates remotePlugins correctly, which is important, and it’s an important helper to decouple our tests IMO

Copy link
Contributor Author

@neelkanth-kaushik neelkanth-kaushik Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-04-23 at 4 15 37 PM I have verified it again that the endpoint "https://cdn.segment.com/v1/projects/foo/settings" is returning 200 OK and there is no error or warning in the Playwright Inspector console as well.

Copy link
Contributor

@silesky silesky Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not following -- great that it works, but is it a problem to use CDNSettingsBuilder instead like we were before?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

const settings = {
integrations: {
FullStory: {
url: 'https://cdn.segment.com/next-integrations/actions/fullstory-plugins/foo.js',
creationName: 'FullStory',
consentSettings: {
categories: ['FooCategory2'],
},
},
{
'Actions Amplitude': {
url: 'https://cdn.segment.com/next-integrations/actions/amplitude-plugins/foo.js',
creationName: 'Actions Amplitude',
consentSettings: {
categories: ['FooCategory1'],
},
}
)
.build()

const mock = await browser.mock('**/settings')
mock.respond(settings, {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
},
},
}

await this.page.route('**/settings', async (route: Route) => {
await route.fulfill({
status: 200,
body: JSON.stringify(settings),
})
})
}

/**
* Hard reload the page
*/
reload() {
return browser.execute(() => window.location.reload())
async reload() {
await this.page.reload()
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { Page } from '@playwright/test'
import { BasePage } from './base-page'

export class ConsentToolsVanilla extends BasePage {
constructor(page: Page, file: string) {
super(page, file)
}

async clickGiveConsent() {
const button = await $('#give-consent')
return button.click()
await this.page.locator('#give-consent').click()
}

async clickDenyConsent() {
const button = await $('#give-consent')
return button.click()
await this.page.locator('#deny-consent').click()
}
}

export class ConsentToolsVanillaOptOut extends ConsentToolsVanilla {
constructor() {
super('consent-tools-vanilla-opt-out.html')
constructor(page: Page) {
super(page, 'consent-tools-vanilla-opt-out.html')
}
}

export class ConsentToolsVanillaOptIn extends ConsentToolsVanilla {
constructor() {
super('consent-tools-vanilla-opt-in.html')
constructor(page: Page) {
super(page, 'consent-tools-vanilla-opt-in.html')
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { Page } from '@playwright/test'
import { BasePage } from './base-page'

class OneTrustPage extends BasePage {
constructor() {
super('onetrust.html')
export class OneTrustPage extends BasePage {
constructor(page: Page) {
super(page, 'onetrust.html')
}

get isOneTrustLoaded(): Promise<void> {
// @ts-ignore
return window.isOneTrustLoaded
// Check if OneTrust is loaded by evaluating a global variable on the page
async isOneTrustLoaded() {
// To Do
}

clickAcceptButtonAndClosePopup() {
return $('#onetrust-accept-btn-handler').click()
//Click the OneTrust accept button
async clickAcceptButtonAndClosePopup() {
// To Do
}
}

export default new OneTrustPage()
Loading
Loading