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

chore: playwright tests #1631

Merged
merged 28 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 20 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
33 changes: 33 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Playwright Tests

on:
pull_request:
push:
branches:
- main

jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 8.x.x
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'pnpm'
- run: pnpm install
- run: pnpm build
- name: Install Playwright Browsers
run: pnpm exec playwright install --with-deps
- name: Run Playwright tests
run: pnpm exec playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ yarn-error.log
stats.html
bundle-stats*.html
.eslintcache
cypress/downloads/downloads.html
cypress/downloads/downloads.html
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ and then `pnpm` commands as usual

## Testing

Unit tests: run `pnpm test`.
Cypress: run `pnpm start` to have a test server running and separately `pnpm cypress` to launch Cypress test engine.
* Unit tests: run `pnpm test`.
* Cypress: run `pnpm start` to have a test server running and separately `pnpm cypress` to launch Cypress test engine.
* Playwright: run e.g. `pnpm exec playwright test --ui --project webkit --project firefox` to run with UI and in webkit and firefox

### Running TestCafe E2E tests with BrowserStack

Expand Down Expand Up @@ -56,7 +57,7 @@ You can use the create react app setup in `playground/nextjs` to test posthog-js
### Tiers of testing

1. Unit tests - this verifies the behavior of the library in bite-sized chunks. Keep this coverage close to 100%, test corner cases and internal behavior here
2. Cypress tests - integrates with a real chrome browser and is capable of testing timing, browser requests, etc. Useful for testing high-level library behavior, ordering and verifying requests. We shouldn't aim for 100% coverage here as it's impossible to test all possible combinations.
2. Browser tests - run in real browsers and so capable of testing timing, browser requests, etc. Useful for testing high-level library behavior, ordering and verifying requests. We shouldn't aim for 100% coverage here as it's impossible to test all possible combinations.
3. TestCafe E2E tests - integrates with a real posthog instance sends data to it. Hardest to write and maintain - keep these very high level

## Developing together with another project
Expand Down
310 changes: 2 additions & 308 deletions cypress/e2e/opting-out.cy.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { assertWhetherPostHogRequestsWereCalled, pollPhCaptures } from '../support/assertions'
import { assertWhetherPostHogRequestsWereCalled } from '../support/assertions'
import { start } from '../support/setup'

function assertThatRecordingStarted() {
cy.phCaptures({ full: true }).then((captures) => {
expect(captures.map((c) => c.event)).to.deep.equal(['$snapshot'])

expect(captures[0]['properties']['$snapshot_data']).to.have.length.above(2)
// a meta and then a full snapshot
expect(captures[0]['properties']['$snapshot_data'][0].type).to.equal(4) // meta
expect(captures[0]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot
})
}

describe('opting out', () => {
describe('session recording', () => {
describe('when starting disabled in some way', () => {
beforeEach(() => {
cy.intercept('POST', '/decide/*', {
editorParams: {},
Expand Down Expand Up @@ -65,301 +54,6 @@ describe('opting out', () => {
})
})

it('does not capture recordings when config disables session recording', () => {
cy.posthogInit({ disable_session_recording: true })

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': false,
'@decide': true,
'@session-recording': false,
})

cy.get('[data-cy-input]')
.type('hello posthog!')
.then(() => {
cy.phCaptures().then((captures) => {
expect(captures || []).to.deep.equal(['$pageview'])
})
})
})

it('can start recording after starting opted out', () => {
cy.posthogInit({ opt_out_capturing_by_default: true })

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': false,
'@decide': true,
'@session-recording': false,
})

cy.posthog().invoke('opt_in_capturing')
// TODO: should we require this call?
cy.posthog().invoke('startSessionRecording')

cy.phCaptures({ full: true }).then((captures) => {
expect((captures || []).map((c) => c.event)).to.deep.equal(['$opt_in', '$pageview'])
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': true,
'@decide': true,
// no call to session-recording yet
})

cy.resetPhCaptures()

cy.get('[data-cy-input]').type('hello posthog!')

pollPhCaptures('$snapshot').then(assertThatRecordingStarted)
})

it('can start recording when starting disabled', () => {
cy.posthogInit({ disable_session_recording: true })

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': false,
'@decide': true,
'@session-recording': false,
})

cy.get('[data-cy-input]')
.type('hello posthog!')
.then(() => {
cy.phCaptures().then((captures) => {
expect(captures || []).to.deep.equal(['$pageview'])
})
})

cy.resetPhCaptures()
cy.posthog().invoke('startSessionRecording')

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': true,
'@decide': true,
// no call to session-recording yet
})

cy.get('[data-cy-input]')
.type('hello posthog!')
.then(() => {
pollPhCaptures('$snapshot').then(assertThatRecordingStarted)
})
})

it('can override sampling when starting session recording', () => {
cy.intercept('POST', '/decide/*', {
autocapture_opt_out: true,
editorParams: {},
isAuthenticated: false,
sessionRecording: {
endpoint: '/ses/',
// will never record a session with rate of 0
sampleRate: '0',
},
}).as('decide')

cy.posthogInit({
opt_out_capturing_by_default: true,
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': false,
'@decide': true,
'@session-recording': false,
})

cy.posthog().invoke('opt_in_capturing')

cy.posthog().invoke('startSessionRecording', { sampling: true })

cy.phCaptures({ full: true }).then((captures) => {
expect((captures || []).map((c) => c.event)).to.deep.equal(['$opt_in', '$pageview'])
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': true,
'@decide': true,
// no call to session-recording yet
})

cy.resetPhCaptures()

cy.get('[data-cy-input]').type('hello posthog!')

pollPhCaptures('$snapshot').then(assertThatRecordingStarted)
})

it('can override linked_flags when starting session recording', () => {
cy.intercept('POST', '/decide/*', {
autocapture_opt_out: true,
editorParams: {},
isAuthenticated: false,
sessionRecording: {
endpoint: '/ses/',
// a flag that doesn't exist, can never be recorded
linkedFlag: 'i am a flag that does not exist',
},
}).as('decide')

cy.posthogInit({
opt_out_capturing_by_default: true,
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': false,
'@decide': true,
'@session-recording': false,
})

cy.posthog().invoke('opt_in_capturing')

cy.posthog().invoke('startSessionRecording')

cy.phCaptures({ full: true }).then((captures) => {
expect((captures || []).map((c) => c.event)).to.deep.equal(['$opt_in', '$pageview'])
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': true,
'@decide': true,
// no call to session-recording yet
})

cy.resetPhCaptures()

cy.get('[data-cy-input]')
.type('hello posthog!')
.then(() => {
cy.phCaptures().then((captures) => {
// no session recording events yet
expect(captures || []).to.deep.equal([])
})
})

cy.posthog().invoke('startSessionRecording', { linked_flag: true })

cy.get('[data-cy-input]').type('hello posthog!')

pollPhCaptures('$snapshot').then(assertThatRecordingStarted)
})

it('respects sampling when overriding linked_flags when starting session recording', () => {
cy.intercept('POST', '/decide/*', {
autocapture_opt_out: true,
editorParams: {},
isAuthenticated: false,
sessionRecording: {
endpoint: '/ses/',
// a flag that doesn't exist, can never be recorded
linkedFlag: 'i am a flag that does not exist',
// will never record a session with rate of 0
sampleRate: '0',
},
}).as('decide')

cy.posthogInit({
opt_out_capturing_by_default: true,
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': false,
'@decide': true,
'@session-recording': false,
})

cy.posthog().invoke('opt_in_capturing')

cy.posthog().invoke('startSessionRecording')

cy.phCaptures({ full: true }).then((captures) => {
expect((captures || []).map((c) => c.event)).to.deep.equal(['$opt_in', '$pageview'])
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': true,
'@decide': true,
// no call to session-recording yet
})

cy.resetPhCaptures()

cy.get('[data-cy-input]')
.type('hello posthog!')
.then(() => {
cy.phCaptures().then((captures) => {
// no session recording events yet
expect(captures || []).to.deep.equal([])
})
})

cy.posthog().invoke('startSessionRecording', { linked_flag: true })

cy.get('[data-cy-input]')
.type('hello posthog!')
.then(() => {
cy.phCaptures().then((captures) => {
// no session recording events yet
expect((captures || []).length).to.equal(0)
})
})
})

it('can override all ingestion controls when starting session recording', () => {
cy.intercept('POST', '/decide/*', {
autocapture_opt_out: true,
editorParams: {},
isAuthenticated: false,
sessionRecording: {
endpoint: '/ses/',
// a flag that doesn't exist, can never be recorded
linkedFlag: 'i am a flag that does not exist',
// will never record a session with rate of 0
sampleRate: '0',
},
}).as('decide')

cy.posthogInit({
opt_out_capturing_by_default: true,
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': false,
'@decide': true,
'@session-recording': false,
})

cy.posthog().invoke('opt_in_capturing')

cy.posthog().invoke('startSessionRecording')

cy.phCaptures({ full: true }).then((captures) => {
expect((captures || []).map((c) => c.event)).to.deep.equal(['$opt_in', '$pageview'])
})

assertWhetherPostHogRequestsWereCalled({
'@recorder-script': true,
'@decide': true,
// no call to session-recording yet
})

cy.resetPhCaptures()

cy.get('[data-cy-input]')
.type('hello posthog!')
.then(() => {
cy.phCaptures().then((captures) => {
// no session recording events yet
expect(captures || []).to.deep.equal([])
})
})

cy.posthog().invoke('startSessionRecording', true)

cy.get('[data-cy-input]').type('hello posthog!')

pollPhCaptures('$snapshot').then(assertThatRecordingStarted)
})

it('sends a $pageview event when opting in', () => {
cy.intercept('POST', '/decide/*', {
autocapture_opt_out: true,
Expand Down
Loading
Loading