playwright-posthog 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 playwright-posthog contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # 🦔 playwright-posthog
2
+
3
+ Playwright matchers for testing PostHog analytics events.
4
+
5
+ ## 📦 Installation
6
+
7
+ ```bash
8
+ npm install --save-dev playwright-posthog
9
+ ```
10
+
11
+ ## 🚀 Setup
12
+
13
+ Extend your Playwright test with PostHog tracking:
14
+
15
+ ```typescript
16
+ // fixtures.ts
17
+ import { test as base, expect as baseExpect } from '@playwright/test';
18
+ import { withPostHogTracking, matchers } from 'playwright-posthog';
19
+
20
+ export const test = withPostHogTracking(base);
21
+ export const expect = baseExpect.extend(matchers);
22
+ ```
23
+
24
+ Then use in your tests:
25
+
26
+ ```typescript
27
+ // my-test.spec.ts
28
+ import { test, expect } from './fixtures';
29
+
30
+ test('user signup tracking works', async ({ page }) => {
31
+ await page.goto('/signup');
32
+ await page.getByLabel('Email').fill('user@example.com');
33
+ await page.getByText('Sign Up').click();
34
+
35
+ // ✅ Assert the event was fired
36
+ await expect(page).toHaveFiredEvent('user_signed_up', {
37
+ plan: 'pro',
38
+ source: 'web'
39
+ });
40
+ });
41
+ ```
42
+
43
+ ## 📖 API Reference
44
+
45
+ ### `withPostHogTracking(test)`
46
+
47
+ Extends a Playwright test instance with PostHog event tracking on the `page` fixture.
48
+
49
+ ```typescript
50
+ import { test as base } from '@playwright/test';
51
+ import { withPostHogTracking } from 'playwright-posthog';
52
+
53
+ export const test = withPostHogTracking(base);
54
+ ```
55
+
56
+ Works with already-extended tests too:
57
+
58
+ ```typescript
59
+ const testWithAuth = base.extend({
60
+ authenticatedPage: async ({ page }, use) => {
61
+ // ... login logic
62
+ await use(page);
63
+ },
64
+ });
65
+
66
+ export const test = withPostHogTracking(testWithAuth);
67
+ ```
68
+
69
+ ### `matchers`
70
+
71
+ Custom matchers to extend Playwright's `expect`:
72
+
73
+ ```typescript
74
+ import { expect as baseExpect } from '@playwright/test';
75
+ import { matchers } from 'playwright-posthog';
76
+
77
+ export const expect = baseExpect.extend(matchers);
78
+ ```
79
+
80
+ ### 🎯 Matchers
81
+
82
+ #### `toHaveFiredEvent(eventName, properties?, config?)`
83
+
84
+ Asserts that a PostHog event was fired. Automatically polls for up to 2 seconds.
85
+
86
+ ```typescript
87
+ // Basic usage
88
+ await expect(page).toHaveFiredEvent('button_clicked');
89
+
90
+ // With property matching (subset match)
91
+ await expect(page).toHaveFiredEvent('purchase_completed', {
92
+ plan: 'pro',
93
+ amount: 99.99
94
+ });
95
+
96
+ // With custom timeout
97
+ await expect(page).toHaveFiredEvent('slow_event', {}, {
98
+ timeout: 5000, // Wait up to 5 seconds
99
+ pollInterval: 200 // Check every 200ms
100
+ });
101
+ ```
102
+
103
+ > 💡 **Properties Matching**: Uses subset matching - the event properties must *contain* the expected properties but can have additional ones.
104
+
105
+ #### `notToHaveFiredEvent(eventName, properties?, config?)`
106
+
107
+ Asserts that an event was NOT fired:
108
+
109
+ ```typescript
110
+ await expect(page).notToHaveFiredEvent('error_occurred');
111
+ ```
112
+
113
+ #### `toHaveCapturedEvents(count?)`
114
+
115
+ Asserts that events were captured:
116
+
117
+ ```typescript
118
+ // At least one event
119
+ await expect(page).toHaveCapturedEvents();
120
+
121
+ // Exactly 5 events
122
+ await expect(page).toHaveCapturedEvents(5);
123
+ ```
124
+
125
+ ### 🐛 Debug Mode
126
+
127
+ Enable debug mode via the `DEBUG` environment variable:
128
+
129
+ ```bash
130
+ DEBUG=true npx playwright test
131
+ ```
132
+
133
+ ### 🛠️ Utility Functions
134
+
135
+ ```typescript
136
+ import { getCapturedEvents, clearCapturedEvents } from 'playwright-posthog';
137
+
138
+ test('advanced usage', async ({ page }) => {
139
+ await page.goto('/');
140
+
141
+ // Get all captured events
142
+ const events = getCapturedEvents(page);
143
+
144
+ // Clear events (useful for multi-step tests)
145
+ clearCapturedEvents(page);
146
+ });
147
+ ```
148
+
149
+ ## 📚 Examples
150
+
151
+ ### Testing Event Properties
152
+
153
+ ```typescript
154
+ test('tracks user preferences', async ({ page }) => {
155
+ await page.goto('/settings');
156
+ await page.getByLabel('Theme').selectOption('dark');
157
+ await page.getByText('Save').click();
158
+
159
+ await expect(page).toHaveFiredEvent('settings_updated', {
160
+ theme: 'dark',
161
+ });
162
+ });
163
+ ```
164
+
165
+ ### Testing a Funnel
166
+
167
+ ```typescript
168
+ test('tracks funnel events', async ({ page }) => {
169
+ await page.goto('/product');
170
+ await expect(page).toHaveFiredEvent('product_viewed');
171
+
172
+ await page.getByText('Add to Cart').click();
173
+ await expect(page).toHaveFiredEvent('add_to_cart');
174
+
175
+ await page.getByText('Checkout').click();
176
+ await expect(page).toHaveFiredEvent('checkout_started');
177
+ });
178
+ ```
179
+
180
+ ## 📄 License
181
+
182
+ MIT
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Type declarations for playwright-posthog custom matchers
3
+ *
4
+ * This file extends Playwright's expect API to include PostHog-specific matchers.
5
+ */
6
+
7
+ import type { Page } from '@playwright/test';
8
+ import type { MatcherConfig } from '../src/matchers';
9
+
10
+ declare global {
11
+ namespace PlaywrightTest {
12
+ interface Matchers<R, T = unknown> {
13
+ /**
14
+ * Assert that a PostHog event was fired with the given name and optional properties.
15
+ *
16
+ * This matcher polls for up to 2 seconds (configurable) to account for the
17
+ * asynchronous nature of analytics events.
18
+ *
19
+ * @param eventName - The name of the event to check for
20
+ * @param expectedProperties - Optional properties to match (subset matching)
21
+ * @param config - Optional configuration for timeout and polling interval
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * await expect(page).toHaveFiredEvent('user_signed_up');
26
+ * await expect(page).toHaveFiredEvent('purchase_completed', {
27
+ * plan: 'pro',
28
+ * amount: 99.99
29
+ * });
30
+ * await expect(page).toHaveFiredEvent('custom_event', {}, {
31
+ * timeout: 5000,
32
+ * pollInterval: 200
33
+ * });
34
+ * ```
35
+ */
36
+ toHaveFiredEvent(
37
+ eventName: string,
38
+ expectedProperties?: Record<string, any>,
39
+ config?: MatcherConfig
40
+ ): Promise<R>;
41
+
42
+ /**
43
+ * Assert that a PostHog event was NOT fired with the given name and optional properties.
44
+ *
45
+ * @param eventName - The name of the event to check for
46
+ * @param expectedProperties - Optional properties to match (subset matching)
47
+ * @param config - Optional configuration for timeout and polling interval
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * await expect(page).notToHaveFiredEvent('error_occurred');
52
+ * ```
53
+ */
54
+ notToHaveFiredEvent(
55
+ eventName: string,
56
+ expectedProperties?: Record<string, any>,
57
+ config?: MatcherConfig
58
+ ): Promise<R>;
59
+
60
+ /**
61
+ * Assert that the page has captured a specific number of events (or any events if count is not provided).
62
+ *
63
+ * @param count - Optional number of events to check for
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * await expect(page).toHaveCapturedEvents(); // At least one event
68
+ * await expect(page).toHaveCapturedEvents(5); // Exactly 5 events
69
+ * ```
70
+ */
71
+ toHaveCapturedEvents(count?: number): R;
72
+ }
73
+ }
74
+ }
75
+
76
+ // This export is necessary to make this a module
77
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { Request } from '@playwright/test';
2
+ import { type CapturedEvent } from './symbols';
3
+ export declare function isPostHogRequest(url: string): boolean;
4
+ export declare function extractEventsFromRequest(request: Request): Promise<CapturedEvent[]>;
5
+ export declare function matchesProperties(actual: Record<string, any> | undefined, expected: Record<string, any> | undefined): boolean;
6
+ //# sourceMappingURL=hog-watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hog-watcher.d.ts","sourceRoot":"","sources":["../src/hog-watcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAM/D,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrD;AAUD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,aAAa,EAAE,CAAC,CAiD1B;AA0BD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EACvC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GACxC,OAAO,CAkBT"}
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPostHogRequest = isPostHogRequest;
4
+ exports.extractEventsFromRequest = extractEventsFromRequest;
5
+ exports.matchesProperties = matchesProperties;
6
+ const symbols_1 = require("./symbols");
7
+ function isPostHogRequest(url) {
8
+ return (url.includes('/e/') ||
9
+ url.includes('/capture') ||
10
+ url.includes('/batch') ||
11
+ url.includes('/s/'));
12
+ }
13
+ async function extractEventsFromRequest(request) {
14
+ const events = [];
15
+ const debug = (0, symbols_1.isDebugEnabled)();
16
+ try {
17
+ const body = request.postDataJSON();
18
+ if (!body) {
19
+ if (debug) {
20
+ console.log('[playwright-posthog] No POST data found in request');
21
+ }
22
+ return events;
23
+ }
24
+ if (debug) {
25
+ console.log('[playwright-posthog] Request body:', JSON.stringify(body, null, 2));
26
+ }
27
+ if (body.batch && Array.isArray(body.batch)) {
28
+ for (const event of body.batch) {
29
+ events.push(normalizeEvent(event));
30
+ }
31
+ }
32
+ else if (Array.isArray(body)) {
33
+ for (const event of body) {
34
+ events.push(normalizeEvent(event));
35
+ }
36
+ }
37
+ else if (body.event || body.e) {
38
+ events.push(normalizeEvent(body));
39
+ }
40
+ else if (body.data) {
41
+ if (Array.isArray(body.data)) {
42
+ for (const event of body.data) {
43
+ events.push(normalizeEvent(event));
44
+ }
45
+ }
46
+ else {
47
+ events.push(normalizeEvent(body.data));
48
+ }
49
+ }
50
+ if (debug && events.length > 0) {
51
+ console.log(`[playwright-posthog] Extracted ${events.length} event(s):`, events.map((e) => e.event).join(', '));
52
+ }
53
+ }
54
+ catch (error) {
55
+ if (debug) {
56
+ console.error('[playwright-posthog] Error parsing PostHog request:', error);
57
+ }
58
+ }
59
+ return events;
60
+ }
61
+ function normalizeEvent(rawEvent) {
62
+ const event = {
63
+ event: rawEvent.event || rawEvent.e || 'unknown',
64
+ properties: rawEvent.properties || rawEvent.p || {},
65
+ timestamp: rawEvent.timestamp || rawEvent.ts || Date.now(),
66
+ distinct_id: rawEvent.distinct_id || rawEvent.d,
67
+ };
68
+ for (const [key, value] of Object.entries(rawEvent)) {
69
+ if (!['event', 'e', 'properties', 'p', 'timestamp', 'ts', 'distinct_id', 'd'].includes(key)) {
70
+ event[key] = value;
71
+ }
72
+ }
73
+ return event;
74
+ }
75
+ function matchesProperties(actual, expected) {
76
+ if (!expected || Object.keys(expected).length === 0) {
77
+ return true;
78
+ }
79
+ if (!actual) {
80
+ return false;
81
+ }
82
+ for (const [key, expectedValue] of Object.entries(expected)) {
83
+ const actualValue = actual[key];
84
+ if (!deepEqual(actualValue, expectedValue)) {
85
+ return false;
86
+ }
87
+ }
88
+ return true;
89
+ }
90
+ function deepEqual(a, b) {
91
+ if (a === b)
92
+ return true;
93
+ if (a == null || b == null)
94
+ return a === b;
95
+ if (typeof a !== typeof b)
96
+ return false;
97
+ if (typeof a !== 'object')
98
+ return a === b;
99
+ if (Array.isArray(a) && Array.isArray(b)) {
100
+ if (a.length !== b.length)
101
+ return false;
102
+ return a.every((val, index) => deepEqual(val, b[index]));
103
+ }
104
+ if (Array.isArray(a) || Array.isArray(b))
105
+ return false;
106
+ const keysA = Object.keys(a);
107
+ const keysB = Object.keys(b);
108
+ if (keysA.length !== keysB.length)
109
+ return false;
110
+ return keysA.every(key => deepEqual(a[key], b[key]));
111
+ }
112
+ //# sourceMappingURL=hog-watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hog-watcher.js","sourceRoot":"","sources":["../src/hog-watcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import { kHogEvents, type CapturedEvent } from './symbols';
2
+ import type { Page, TestType } from '@playwright/test';
3
+ export { matchers } from './matchers';
4
+ export type HogPage = Page & {
5
+ [kHogEvents]: CapturedEvent[];
6
+ };
7
+ export declare function withPostHogTracking<T extends TestType<any, any>>(test: T): T;
8
+ export declare function getCapturedEvents(page: Page): CapturedEvent[];
9
+ export declare function clearCapturedEvents(page: Page): void;
10
+ export type { CapturedEvent } from './symbols';
11
+ export type { MatcherConfig } from './matchers';
12
+ export { isPostHogRequest, extractEventsFromRequest, matchesProperties } from './hog-watcher';
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiCA,OAAO,EAAE,UAAU,EAAkB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAE3E,OAAO,KAAK,EAAE,IAAI,EAAkB,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEvE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAKtC,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG;IAC3B,CAAC,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;CAC/B,CAAC;AAiBF,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,CA2C5E;AAKD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,aAAa,EAAE,CAG7D;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAKpD;AAKD,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchesProperties = exports.extractEventsFromRequest = exports.isPostHogRequest = exports.matchers = void 0;
4
+ exports.withPostHogTracking = withPostHogTracking;
5
+ exports.getCapturedEvents = getCapturedEvents;
6
+ exports.clearCapturedEvents = clearCapturedEvents;
7
+ const symbols_1 = require("./symbols");
8
+ const hog_watcher_1 = require("./hog-watcher");
9
+ var matchers_1 = require("./matchers");
10
+ Object.defineProperty(exports, "matchers", { enumerable: true, get: function () { return matchers_1.matchers; } });
11
+ function withPostHogTracking(test) {
12
+ return test.extend({
13
+ page: async ({ page: originalPage }, use) => {
14
+ const hogPage = originalPage;
15
+ const debug = (0, symbols_1.isDebugEnabled)();
16
+ hogPage[symbols_1.kHogEvents] = [];
17
+ if (debug) {
18
+ console.log('[playwright-posthog] Initializing PostHog event capture');
19
+ }
20
+ await hogPage.route('**/*', async (route, request) => {
21
+ const url = request.url();
22
+ if ((0, hog_watcher_1.isPostHogRequest)(url)) {
23
+ if (debug) {
24
+ console.log(`[playwright-posthog] Intercepted PostHog request: ${url}`);
25
+ }
26
+ const events = await (0, hog_watcher_1.extractEventsFromRequest)(request);
27
+ hogPage[symbols_1.kHogEvents].push(...events);
28
+ if (debug && events.length > 0) {
29
+ console.log(`[playwright-posthog] Total events captured: ${hogPage[symbols_1.kHogEvents].length}`);
30
+ console.log(`[playwright-posthog] Latest events:`, events.map((e) => ({
31
+ event: e.event,
32
+ properties: e.properties,
33
+ })));
34
+ }
35
+ }
36
+ await route.continue();
37
+ });
38
+ await use(hogPage);
39
+ if (debug) {
40
+ console.log(`[playwright-posthog] Test complete. Total events captured: ${hogPage[symbols_1.kHogEvents].length}`);
41
+ }
42
+ },
43
+ });
44
+ }
45
+ function getCapturedEvents(page) {
46
+ const hogPage = page;
47
+ return hogPage[symbols_1.kHogEvents] || [];
48
+ }
49
+ function clearCapturedEvents(page) {
50
+ const hogPage = page;
51
+ if (hogPage[symbols_1.kHogEvents]) {
52
+ hogPage[symbols_1.kHogEvents] = [];
53
+ }
54
+ }
55
+ var hog_watcher_2 = require("./hog-watcher");
56
+ Object.defineProperty(exports, "isPostHogRequest", { enumerable: true, get: function () { return hog_watcher_2.isPostHogRequest; } });
57
+ Object.defineProperty(exports, "extractEventsFromRequest", { enumerable: true, get: function () { return hog_watcher_2.extractEventsFromRequest; } });
58
+ Object.defineProperty(exports, "matchesProperties", { enumerable: true, get: function () { return hog_watcher_2.matchesProperties; } });
59
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AA6DA,kDA2CC;AAKD,8CAGC;AAKD,kDAKC;AAzFD,uCAA2E;AAC3E,+CAA2E;AAG3E,uCAAsC;AAA7B,oGAAA,QAAQ,OAAA;AAwBjB,SAAgB,mBAAmB,CAA+B,IAAO;IACvE,OAAO,IAAI,CAAC,MAAM,CAAC;QACjB,IAAI,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,YAAY,EAAkB,EAAE,GAAqC,EAAE,EAAE;YAC5F,MAAM,OAAO,GAAG,YAAuB,CAAC;YACxC,MAAM,KAAK,GAAG,IAAA,wBAAc,GAAE,CAAC;YAE/B,OAAO,CAAC,oBAAU,CAAC,GAAG,EAAE,CAAC;YAEzB,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,GAAG,CAAC,yDAAyD,CAAC,CAAC;YACzE,CAAC;YAED,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAY,EAAE,OAAgB,EAAE,EAAE;gBACnE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;gBAE1B,IAAI,IAAA,8BAAgB,EAAC,GAAG,CAAC,EAAE,CAAC;oBAC1B,IAAI,KAAK,EAAE,CAAC;wBACV,OAAO,CAAC,GAAG,CAAC,qDAAqD,GAAG,EAAE,CAAC,CAAC;oBAC1E,CAAC;oBAED,MAAM,MAAM,GAAG,MAAM,IAAA,sCAAwB,EAAC,OAAO,CAAC,CAAC;oBAEvD,OAAO,CAAC,oBAAU,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC;oBAEpC,IAAI,KAAK,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBAC/B,OAAO,CAAC,GAAG,CAAC,+CAA+C,OAAO,CAAC,oBAAU,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;wBACzF,OAAO,CAAC,GAAG,CAAC,qCAAqC,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC;4BACnF,KAAK,EAAE,CAAC,CAAC,KAAK;4BACd,UAAU,EAAE,CAAC,CAAC,UAAU;yBACzB,CAAC,CAAC,CAAC,CAAC;oBACP,CAAC;gBACH,CAAC;gBAED,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC;YACzB,CAAC,CAAC,CAAC;YAEH,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC;YAEnB,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,GAAG,CAAC,8DAA8D,OAAO,CAAC,oBAAU,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1G,CAAC;QACH,CAAC;KACF,CAAM,CAAC;AACV,CAAC;AAKD,SAAgB,iBAAiB,CAAC,IAAU;IAC1C,MAAM,OAAO,GAAG,IAAe,CAAC;IAChC,OAAO,OAAO,CAAC,oBAAU,CAAC,IAAI,EAAE,CAAC;AACnC,CAAC;AAKD,SAAgB,mBAAmB,CAAC,IAAU;IAC5C,MAAM,OAAO,GAAG,IAAe,CAAC;IAChC,IAAI,OAAO,CAAC,oBAAU,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,oBAAU,CAAC,GAAG,EAAE,CAAC;IAC3B,CAAC;AACH,CAAC;AAOD,6CAA8F;AAArF,+GAAA,gBAAgB,OAAA;AAAE,uHAAA,wBAAwB,OAAA;AAAE,gHAAA,iBAAiB,OAAA"}
@@ -0,0 +1,24 @@
1
+ import type { Page } from '@playwright/test';
2
+ export interface MatcherConfig {
3
+ timeout?: number;
4
+ pollInterval?: number;
5
+ }
6
+ declare function toHaveFiredEvent(page: Page, eventName: string, expectedProperties?: Record<string, any>, config?: MatcherConfig): Promise<{
7
+ pass: boolean;
8
+ message: () => string;
9
+ }>;
10
+ declare function notToHaveFiredEvent(page: Page, eventName: string, expectedProperties?: Record<string, any>, config?: MatcherConfig): Promise<{
11
+ pass: boolean;
12
+ message: () => string;
13
+ }>;
14
+ declare function toHaveCapturedEvents(page: Page, count?: number): {
15
+ pass: boolean;
16
+ message: () => string;
17
+ };
18
+ export declare const matchers: {
19
+ toHaveFiredEvent: typeof toHaveFiredEvent;
20
+ notToHaveFiredEvent: typeof notToHaveFiredEvent;
21
+ toHaveCapturedEvents: typeof toHaveCapturedEvents;
22
+ };
23
+ export {};
24
+ //# sourceMappingURL=matchers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matchers.d.ts","sourceRoot":"","sources":["../src/matchers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAQ7C,MAAM,WAAW,aAAa;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA2DD,iBAAe,gBAAgB,CAC7B,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,MAAM,EACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACxC,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,MAAM,CAAA;CAAE,CAAC,CAwCnD;AAED,iBAAe,mBAAmB,CAChC,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,MAAM,EACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACxC,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,MAAM,CAAA;CAAE,CAAC,CAGnD;AAED,iBAAS,oBAAoB,CAC3B,IAAI,EAAE,IAAI,EACV,KAAK,CAAC,EAAE,MAAM,GACb;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,MAAM,CAAA;CAAE,CAkB1C;AAED,eAAO,MAAM,QAAQ;;;;CAIpB,CAAC"}
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchers = void 0;
4
+ const symbols_1 = require("./symbols");
5
+ const hog_watcher_1 = require("./hog-watcher");
6
+ const DEFAULT_CONFIG = {
7
+ timeout: 2000,
8
+ pollInterval: 100,
9
+ };
10
+ function formatProps(props) {
11
+ return props ? ` with properties ${JSON.stringify(props)}` : '';
12
+ }
13
+ function formatEventList(events) {
14
+ return events.map(e => e.event).join(', ');
15
+ }
16
+ function buildFailureMessage(eventName, expectedProperties, timeout, events) {
17
+ const lines = [
18
+ `Expected page to have fired event "${eventName}"${formatProps(expectedProperties)}, but it did not.`,
19
+ '',
20
+ `Waited ${timeout}ms and captured ${events.length} total event(s).`,
21
+ '',
22
+ ];
23
+ if (events.length === 0) {
24
+ lines.push('No PostHog events were captured at all. Possible reasons:', ' • PostHog is not initialized on the page', ' • The event is sent to a different endpoint', ' • Network interception is not working');
25
+ }
26
+ else {
27
+ const matchingEvents = events.filter(e => e.event === eventName);
28
+ if (matchingEvents.length > 0) {
29
+ lines.push(`Found ${matchingEvents.length} event(s) named "${eventName}" but properties didn't match:`, '');
30
+ matchingEvents.forEach((event, i) => {
31
+ lines.push(` Event ${i + 1}: ${JSON.stringify(event.properties, null, 2)}`);
32
+ });
33
+ lines.push('', `Expected: ${JSON.stringify(expectedProperties, null, 2)}`);
34
+ }
35
+ else {
36
+ lines.push(`Events captured: [${formatEventList(events)}]`, '', 'The event name did not match any captured events.');
37
+ }
38
+ }
39
+ return lines.join('\n');
40
+ }
41
+ async function toHaveFiredEvent(page, eventName, expectedProperties, config) {
42
+ const hogPage = page;
43
+ const { timeout, pollInterval } = { ...DEFAULT_CONFIG, ...config };
44
+ const debug = (0, symbols_1.isDebugEnabled)();
45
+ if (debug) {
46
+ console.log(`[playwright-posthog] Looking for: "${eventName}"${formatProps(expectedProperties)}`);
47
+ }
48
+ const startTime = Date.now();
49
+ let foundEvent;
50
+ while (Date.now() - startTime < timeout) {
51
+ const events = hogPage[symbols_1.kHogEvents] || [];
52
+ foundEvent = events.find((event) => {
53
+ return event.event === eventName && (0, hog_watcher_1.matchesProperties)(event.properties, expectedProperties);
54
+ });
55
+ if (foundEvent) {
56
+ if (debug) {
57
+ console.log(`[playwright-posthog] ✓ Found after ${Date.now() - startTime}ms`);
58
+ }
59
+ break;
60
+ }
61
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
62
+ }
63
+ const pass = !!foundEvent;
64
+ return {
65
+ pass,
66
+ message: () => {
67
+ if (pass) {
68
+ return `Expected page NOT to have fired event "${eventName}"${formatProps(expectedProperties)}, but it did.`;
69
+ }
70
+ return buildFailureMessage(eventName, expectedProperties, timeout, hogPage[symbols_1.kHogEvents] || []);
71
+ },
72
+ };
73
+ }
74
+ async function notToHaveFiredEvent(page, eventName, expectedProperties, config) {
75
+ const result = await toHaveFiredEvent(page, eventName, expectedProperties, config);
76
+ return { pass: !result.pass, message: result.message };
77
+ }
78
+ function toHaveCapturedEvents(page, count) {
79
+ const events = page[symbols_1.kHogEvents] || [];
80
+ const actual = events.length;
81
+ const pass = count === undefined ? actual > 0 : actual === count;
82
+ return {
83
+ pass,
84
+ message: () => {
85
+ if (count === undefined) {
86
+ return pass
87
+ ? `Expected no events, but captured ${actual}.`
88
+ : `Expected events, but none were captured.`;
89
+ }
90
+ return pass
91
+ ? `Expected NOT to capture ${count} event(s), but did.`
92
+ : `Expected ${count} event(s), got ${actual}. Events: [${formatEventList(events)}]`;
93
+ },
94
+ };
95
+ }
96
+ exports.matchers = {
97
+ toHaveFiredEvent,
98
+ notToHaveFiredEvent,
99
+ toHaveCapturedEvents,
100
+ };
101
+ //# sourceMappingURL=matchers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matchers.js","sourceRoot":"","sources":["../src/matchers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare const kHogEvents: unique symbol;
2
+ export declare function isDebugEnabled(): boolean;
3
+ export interface CapturedEvent {
4
+ event: string;
5
+ properties?: Record<string, any>;
6
+ timestamp?: string | number;
7
+ distinct_id?: string;
8
+ [key: string]: any;
9
+ }
10
+ //# sourceMappingURL=symbols.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symbols.d.ts","sourceRoot":"","sources":["../src/symbols.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,UAAU,eAAuB,CAAC;AAM/C,wBAAgB,cAAc,IAAI,OAAO,CAGxC;AAKD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.kHogEvents = void 0;
4
+ exports.isDebugEnabled = isDebugEnabled;
5
+ exports.kHogEvents = Symbol('hog:events');
6
+ function isDebugEnabled() {
7
+ const debug = process.env.DEBUG;
8
+ return debug === 'true' || debug === '1' || debug === 'playwright-posthog';
9
+ }
10
+ //# sourceMappingURL=symbols.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symbols.js","sourceRoot":"","sources":["../src/symbols.ts"],"names":[],"mappings":";;;AAeA,wCAGC;AATY,QAAA,UAAU,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAM/C,SAAgB,cAAc;IAC5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;IAChC,OAAO,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,oBAAoB,CAAC;AAC7E,CAAC"}
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Example test file demonstrating playwright-posthog usage
3
+ *
4
+ * This file shows various patterns for testing PostHog analytics
5
+ * events in your Playwright tests.
6
+ */
7
+
8
+ import { test as base, expect as baseExpect } from '@playwright/test';
9
+ import { withPostHogTracking, matchers } from 'playwright-posthog';
10
+
11
+ // Setup: extend your test with PostHog tracking
12
+ const test = withPostHogTracking(base);
13
+ const expect = baseExpect.extend(matchers);
14
+
15
+ test.describe('PostHog Analytics Tracking', () => {
16
+ test('basic event tracking', async ({ page }) => {
17
+ await page.goto('https://example.com');
18
+
19
+ await page.getByText('Sign Up').click();
20
+
21
+ await expect(page).toHaveFiredEvent('signup_clicked');
22
+ });
23
+
24
+ test('event with properties', async ({ page }) => {
25
+ await page.goto('https://example.com/pricing');
26
+
27
+ await page.getByTestId('plan-pro').click();
28
+
29
+ await expect(page).toHaveFiredEvent('plan_selected', {
30
+ plan: 'pro',
31
+ price: 99.99,
32
+ });
33
+ });
34
+
35
+ test('multiple events in sequence', async ({ page }) => {
36
+ await page.goto('https://example.com/product/123');
37
+
38
+ await expect(page).toHaveFiredEvent('product_viewed', {
39
+ product_id: '123',
40
+ });
41
+
42
+ await page.getByText('Add to Cart').click();
43
+
44
+ await expect(page).toHaveFiredEvent('add_to_cart', {
45
+ product_id: '123',
46
+ });
47
+
48
+ await page.getByText('Checkout').click();
49
+ await expect(page).toHaveFiredEvent('checkout_started');
50
+ });
51
+
52
+ test('negative assertion - event should NOT fire', async ({ page }) => {
53
+ await page.goto('https://example.com');
54
+
55
+ await page.getByText('About').click();
56
+
57
+ await expect(page).notToHaveFiredEvent('error_occurred');
58
+ });
59
+
60
+ test('custom timeout for slow events', async ({ page }) => {
61
+ await page.goto('https://example.com');
62
+
63
+ await page.getByText('Submit').click();
64
+
65
+ await expect(page).toHaveFiredEvent(
66
+ 'form_submitted',
67
+ { form_type: 'contact' },
68
+ { timeout: 5000 }
69
+ );
70
+ });
71
+
72
+ test('partial property matching', async ({ page }) => {
73
+ await page.goto('https://example.com/settings');
74
+
75
+ await page.getByLabel('Theme').selectOption('dark');
76
+ await page.getByText('Save').click();
77
+
78
+ await expect(page).toHaveFiredEvent('settings_updated', {
79
+ theme: 'dark',
80
+ });
81
+ });
82
+
83
+ test('checking event count', async ({ page }) => {
84
+ await page.goto('https://example.com');
85
+
86
+ await expect(page).toHaveCapturedEvents();
87
+
88
+ await expect(page).toHaveCapturedEvents(3);
89
+ });
90
+ });
91
+
92
+ test.describe('Advanced Usage', () => {
93
+ test('using utility functions', async ({ page }) => {
94
+ const { getCapturedEvents, clearCapturedEvents } = await import('playwright-posthog');
95
+
96
+ await page.goto('https://example.com/step1');
97
+
98
+ let events = getCapturedEvents(page);
99
+ console.log('Step 1 events:', events);
100
+
101
+ clearCapturedEvents(page);
102
+
103
+ await page.goto('https://example.com/step2');
104
+
105
+ events = getCapturedEvents(page);
106
+ console.log('Step 2 events:', events);
107
+
108
+ await expect(page).toHaveFiredEvent('step2_viewed');
109
+ });
110
+
111
+ test('complex property matching', async ({ page }) => {
112
+ await page.goto('https://example.com/checkout');
113
+
114
+ await page.fill('[name=email]', 'user@example.com');
115
+ await page.fill('[name=card]', '4242424242424242');
116
+ await page.getByText('Pay').click();
117
+
118
+ await expect(page).toHaveFiredEvent('payment_submitted', {
119
+ email: 'user@example.com',
120
+ payment_method: 'card',
121
+ metadata: {
122
+ source: 'web',
123
+ },
124
+ });
125
+ });
126
+ });
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Example Playwright configuration for playwright-posthog
3
+ *
4
+ * See https://playwright.dev/docs/test-configuration
5
+ */
6
+
7
+ import { defineConfig, devices } from '@playwright/test';
8
+
9
+ export default defineConfig({
10
+ testDir: './tests',
11
+
12
+ // Run tests in files in parallel
13
+ fullyParallel: true,
14
+
15
+ // Fail the build on CI if you accidentally left test.only in the source code
16
+ forbidOnly: !!process.env.CI,
17
+
18
+ // Retry on CI only
19
+ retries: process.env.CI ? 2 : 0,
20
+
21
+ // Opt out of parallel tests on CI
22
+ workers: process.env.CI ? 1 : undefined,
23
+
24
+ // Reporter to use
25
+ reporter: 'html',
26
+
27
+ use: {
28
+ // Base URL to use in actions like `await page.goto('/')`
29
+ baseURL: 'http://localhost:3000',
30
+
31
+ // Collect trace when retrying the failed test
32
+ trace: 'on-first-retry',
33
+ },
34
+
35
+ // Configure projects for major browsers
36
+ projects: [
37
+ {
38
+ name: 'chromium',
39
+ use: { ...devices['Desktop Chrome'] },
40
+ },
41
+
42
+ {
43
+ name: 'firefox',
44
+ use: { ...devices['Desktop Firefox'] },
45
+ },
46
+
47
+ {
48
+ name: 'webkit',
49
+ use: { ...devices['Desktop Safari'] },
50
+ },
51
+
52
+ // Test against mobile viewports
53
+ {
54
+ name: 'Mobile Chrome',
55
+ use: { ...devices['Pixel 5'] },
56
+ },
57
+ {
58
+ name: 'Mobile Safari',
59
+ use: { ...devices['iPhone 12'] },
60
+ },
61
+ ],
62
+
63
+ // Run your local dev server before starting the tests
64
+ webServer: {
65
+ command: 'npm run dev',
66
+ url: 'http://localhost:3000',
67
+ reuseExistingServer: !process.env.CI,
68
+ },
69
+ });
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "playwright-posthog",
3
+ "version": "1.0.0",
4
+ "description": "Test PostHog analytics events in your Playwright tests with type-safe assertions and automatic event capture",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "examples",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc && cp src/global.d.ts dist/global.d.ts",
23
+ "clean": "rm -rf dist",
24
+ "rebuild": "npm run clean && npm run build",
25
+ "prepublishOnly": "npm run rebuild",
26
+ "test": "npm run build && playwright test",
27
+ "test:ui": "npm run build && playwright test --ui",
28
+ "test:debug": "npm run build && playwright test --debug",
29
+ "serve": "npx http-server test-app -p 8080"
30
+ },
31
+ "keywords": [
32
+ "playwright",
33
+ "posthog",
34
+ "analytics",
35
+ "testing",
36
+ "e2e",
37
+ "tracking",
38
+ "test",
39
+ "fixture",
40
+ "matcher",
41
+ "assertions",
42
+ "event-tracking",
43
+ "product-analytics",
44
+ "typescript"
45
+ ],
46
+ "author": "playwright-posthog contributors",
47
+ "license": "MIT",
48
+ "engines": {
49
+ "node": ">=16.0.0"
50
+ },
51
+ "peerDependencies": {
52
+ "@playwright/test": "^1.40.0"
53
+ },
54
+ "devDependencies": {
55
+ "@playwright/test": "^1.40.0",
56
+ "@types/node": "^25.0.3",
57
+ "http-server": "^14.1.1",
58
+ "typescript": "^5.3.0"
59
+ },
60
+ "repository": {
61
+ "type": "git",
62
+ "url": "git+https://github.com/yourusername/playwright-posthog.git"
63
+ },
64
+ "bugs": {
65
+ "url": "https://github.com/yourusername/playwright-posthog/issues"
66
+ },
67
+ "homepage": "https://github.com/yourusername/playwright-posthog#readme"
68
+ }