From ab44eadc233ea507405af281e3f7da2a9b4b35eb Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:33:33 +0530 Subject: [PATCH 1/5] test(e2e): test on monetization event, add custom spy matchers --- package.json | 1 + pnpm-lock.yaml | 9 +++ tests/e2e/fixtures/base.ts | 134 +++++++++++++++++++++++++++++++++---- tests/e2e/simple.spec.ts | 18 ++++- 4 files changed, 146 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 7f5b1457..e30b79b8 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "react-scan": "^0.0.35", "sade": "^1.8.1", "tailwindcss": "^3.4.17", + "tinyspy": "^3.0.2", "ts-jest": "^29.2.5", "tsx": "^4.19.2", "typescript": "^5.7.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f9192dc..44d65402 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -196,6 +196,9 @@ importers: tailwindcss: specifier: ^3.4.17 version: 3.4.17(ts-node@10.9.2(@types/node@20.17.12)(typescript@5.7.2)) + tinyspy: + specifier: ^3.0.2 + version: 3.0.2 ts-jest: specifier: ^29.2.5 version: 29.2.5(@babel/core@7.24.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.0))(esbuild@0.24.2)(jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@types/node@20.17.12)(typescript@5.7.2)))(typescript@5.7.2) @@ -3995,6 +3998,10 @@ packages: thread-stream@2.4.1: resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -8637,6 +8644,8 @@ snapshots: dependencies: real-require: 0.2.0 + tinyspy@3.0.2: {} + tmpl@1.0.5: {} to-fast-properties@2.0.0: {} diff --git a/tests/e2e/fixtures/base.ts b/tests/e2e/fixtures/base.ts index fb0256e4..3d0a9791 100644 --- a/tests/e2e/fixtures/base.ts +++ b/tests/e2e/fixtures/base.ts @@ -1,4 +1,5 @@ import { test as base, type BrowserContext, type Page } from '@playwright/test'; +import type { SpyFn } from 'tinyspy'; import { getBackground, getStorage, @@ -7,6 +8,7 @@ import { type Background, } from './helpers'; import { openPopup, type Popup } from '../pages/popup'; +import { sleep } from '@/shared/helpers'; import type { DeepPartial, Storage } from '@/shared/types'; type BaseScopeWorker = { @@ -89,23 +91,127 @@ export const expect = test.expect.extend({ const message = pass ? () => - this.utils.matcherHint(assertionName, undefined, undefined, { + `${this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot, - }) + - '\n\n' + - `Expected: not ${this.utils.printExpected(expected)}\n` + - (matcherResult - ? `Received: ${this.utils.printReceived(matcherResult.actual)}` - : '') + })}\n\nExpected: not ${this.utils.printExpected(expected)}\n${ + matcherResult + ? `Received: ${this.utils.printReceived(matcherResult.actual)}` + : '' + }` : () => - this.utils.matcherHint(assertionName, undefined, undefined, { + `${this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot, - }) + - '\n\n' + - `Expected: ${this.utils.printExpected(expected)}\n` + - (matcherResult - ? `Received: ${this.utils.printReceived(matcherResult.actual)}` - : ''); + })}\n\nExpected: ${this.utils.printExpected(expected)}\n${ + matcherResult + ? `Received: ${this.utils.printReceived(matcherResult.actual)}` + : '' + }`; + + return { + name: assertionName, + pass, + expected, + actual: matcherResult?.actual, + message, + }; + }, + + async toHaveBeenCalledTimes( + fn: SpyFn, + expectedTimes: number, + { timeout = 5000, wait = 1000 }: { timeout?: number; wait?: number } = {}, + ) { + const assertionName = 'toHaveBeenCalledTimes'; + + let pass: boolean; + let matcherResult: { actual: number } | undefined; + + await sleep(wait); + let remainingTime = timeout; + do { + try { + test.expect(fn.callCount).toBe(expectedTimes); + pass = true; + break; + } catch { + matcherResult = { actual: fn.callCount }; + pass = false; + remainingTime -= 500; + await sleep(500); + } + } while (remainingTime > 0); + + const message = pass + ? () => + `${this.utils.matcherHint(assertionName, undefined, undefined, { + isNot: this.isNot, + })}\n\nExpected: not ${this.utils.printExpected(expectedTimes)}\n${ + matcherResult + ? `Received: ${this.utils.printReceived(matcherResult.actual)}` + : '' + }` + : () => + `${this.utils.matcherHint(assertionName, undefined, undefined, { + isNot: this.isNot, + })}\n\nExpected: ${this.utils.printExpected(expectedTimes)}\n${ + matcherResult + ? `Received: ${this.utils.printReceived(matcherResult.actual)}` + : '' + }`; + + return { + name: assertionName, + pass, + expected: expectedTimes, + actual: matcherResult?.actual, + message, + }; + }, + + async toHaveBeenLastCalledWithMatching( + fn: SpyFn, + expected: Record, + { timeout = 5000, wait = 1000 }: { timeout?: number; wait?: number } = {}, + ) { + const assertionName = 'toHaveBeenLastCalledWithMatching'; + + let pass: boolean; + let matcherResult: { actual: unknown } | undefined; + + await sleep(wait); + let remainingTime = timeout; + do { + try { + // we only support matching first argument of last call + const lastCallArg = fn.calls[fn.calls.length - 1][0]; + test.expect(lastCallArg).toMatchObject(expected); + pass = true; + break; + } catch { + matcherResult = { actual: fn.calls[fn.calls.length - 1]?.[0] }; + pass = false; + remainingTime -= 500; + await sleep(500); + } + } while (remainingTime > 0); + + const message = pass + ? () => + `${this.utils.matcherHint(assertionName, undefined, undefined, { + isNot: this.isNot, + })}\n\nExpected: not ${this.utils.printExpected(expected)}\n${ + matcherResult + ? `Received: ${this.utils.printReceived(matcherResult.actual)}` + : '' + }` + : () => + `${this.utils.matcherHint(assertionName, undefined, undefined, { + isNot: this.isNot, + })}\n\nExpected: ${this.utils.printExpected(expected)}\n${ + matcherResult + ? `Received: ${this.utils.printReceived(matcherResult.actual)}` + : '' + }`; return { name: assertionName, diff --git a/tests/e2e/simple.spec.ts b/tests/e2e/simple.spec.ts index bc6330b1..76f93f3f 100644 --- a/tests/e2e/simple.spec.ts +++ b/tests/e2e/simple.spec.ts @@ -1,3 +1,4 @@ +import { spy } from 'tinyspy'; import { test, expect } from './fixtures/connected'; test.beforeEach(async ({ popup }) => { @@ -13,15 +14,18 @@ test('should monetize site with single wallet address', async ({ await page.goto(playgroundUrl); - const monetizationCallback = (ev: any) => ev; + const monetizationCallback = spy<[Event], void>(); await page.exposeFunction('monetizationCallback', monetizationCallback); + await page.evaluate(() => { + window.addEventListener('monetization', monetizationCallback); + }); await page .getByLabel('Wallet address/Payment pointer') .fill(walletAddressUrl); await page.getByRole('button', { name: 'Add monetization link' }).click(); - await expect(page.locator(`link[rel=monetization]`)).toHaveAttribute( + await expect(page.locator('link[rel=monetization]')).toHaveAttribute( 'href', walletAddressUrl, ); @@ -32,6 +36,16 @@ test('should monetize site with single wallet address', async ({ 'Load Event', ); + await expect(monetizationCallback).toHaveBeenCalledTimes(1); + await expect(monetizationCallback).toHaveBeenLastCalledWithMatching({ + paymentPointer: walletAddressUrl, + amountSent: { + currency: expect.stringMatching(/^[A-Z]{3}$/), + value: expect.stringMatching(/^0\.\d+$/), + }, + incomingPayment: expect.stringContaining(new URL(walletAddressUrl).origin), + }); + await popup.reload({ waitUntil: 'networkidle' }); await page.bringToFront(); await popup.waitForSelector(`[data-testid="home-page"]`); From ba0351cba8b9c4eaa15aaf3362ac25bff7584825 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:58:33 +0530 Subject: [PATCH 2/5] refactor: extract common defaultMessage --- tests/e2e/fixtures/base.ts | 108 ++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 61 deletions(-) diff --git a/tests/e2e/fixtures/base.ts b/tests/e2e/fixtures/base.ts index 3d0a9791..01b4f22a 100644 --- a/tests/e2e/fixtures/base.ts +++ b/tests/e2e/fixtures/base.ts @@ -1,4 +1,9 @@ -import { test as base, type BrowserContext, type Page } from '@playwright/test'; +import { + test as base, + type ExpectMatcherState, + type BrowserContext, + type Page, +} from '@playwright/test'; import type { SpyFn } from 'tinyspy'; import { getBackground, @@ -70,6 +75,23 @@ export const test = base.extend<{ page: Page }, BaseScopeWorker>({ }, }); +const defaultMessage = ( + thisType: ExpectMatcherState, + assertionName: string, + pass: boolean, + expected: unknown, + matcherResult?: { actual: unknown }, +) => { + return () => + `${thisType.utils.matcherHint(assertionName, undefined, undefined, { + isNot: thisType.isNot, + })}\n\nExpected:${pass ? '' : ' not '}${thisType.utils.printExpected(expected)}\n${ + matcherResult + ? `Received: ${thisType.utils.printReceived(matcherResult.actual)}` + : '' + }`; +}; + export const expect = test.expect.extend({ async toHaveStorage(background: Background, expected: DeepPartial) { const assertionName = 'toHaveStorage'; @@ -89,36 +111,24 @@ export const expect = test.expect.extend({ pass = false; } - const message = pass - ? () => - `${this.utils.matcherHint(assertionName, undefined, undefined, { - isNot: this.isNot, - })}\n\nExpected: not ${this.utils.printExpected(expected)}\n${ - matcherResult - ? `Received: ${this.utils.printReceived(matcherResult.actual)}` - : '' - }` - : () => - `${this.utils.matcherHint(assertionName, undefined, undefined, { - isNot: this.isNot, - })}\n\nExpected: ${this.utils.printExpected(expected)}\n${ - matcherResult - ? `Received: ${this.utils.printReceived(matcherResult.actual)}` - : '' - }`; - return { name: assertionName, pass, expected, actual: matcherResult?.actual, - message, + message: defaultMessage( + this, + assertionName, + pass, + expected, + matcherResult, + ), }; }, async toHaveBeenCalledTimes( fn: SpyFn, - expectedTimes: number, + expected: number, { timeout = 5000, wait = 1000 }: { timeout?: number; wait?: number } = {}, ) { const assertionName = 'toHaveBeenCalledTimes'; @@ -130,7 +140,7 @@ export const expect = test.expect.extend({ let remainingTime = timeout; do { try { - test.expect(fn.callCount).toBe(expectedTimes); + test.expect(fn.callCount).toBe(expected); pass = true; break; } catch { @@ -141,30 +151,18 @@ export const expect = test.expect.extend({ } } while (remainingTime > 0); - const message = pass - ? () => - `${this.utils.matcherHint(assertionName, undefined, undefined, { - isNot: this.isNot, - })}\n\nExpected: not ${this.utils.printExpected(expectedTimes)}\n${ - matcherResult - ? `Received: ${this.utils.printReceived(matcherResult.actual)}` - : '' - }` - : () => - `${this.utils.matcherHint(assertionName, undefined, undefined, { - isNot: this.isNot, - })}\n\nExpected: ${this.utils.printExpected(expectedTimes)}\n${ - matcherResult - ? `Received: ${this.utils.printReceived(matcherResult.actual)}` - : '' - }`; - return { name: assertionName, pass, - expected: expectedTimes, + expected, actual: matcherResult?.actual, - message, + message: defaultMessage( + this, + assertionName, + pass, + expected, + matcherResult, + ), }; }, @@ -195,30 +193,18 @@ export const expect = test.expect.extend({ } } while (remainingTime > 0); - const message = pass - ? () => - `${this.utils.matcherHint(assertionName, undefined, undefined, { - isNot: this.isNot, - })}\n\nExpected: not ${this.utils.printExpected(expected)}\n${ - matcherResult - ? `Received: ${this.utils.printReceived(matcherResult.actual)}` - : '' - }` - : () => - `${this.utils.matcherHint(assertionName, undefined, undefined, { - isNot: this.isNot, - })}\n\nExpected: ${this.utils.printExpected(expected)}\n${ - matcherResult - ? `Received: ${this.utils.printReceived(matcherResult.actual)}` - : '' - }`; - return { name: assertionName, pass, expected, actual: matcherResult?.actual, - message, + message: defaultMessage( + this, + assertionName, + pass, + expected, + matcherResult, + ), }; }, }); From 0ee50f1c0a2eb4e6c0155d66dceab6b07314421e Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:01:45 +0530 Subject: [PATCH 3/5] simplify defaultMessage --- tests/e2e/fixtures/base.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/e2e/fixtures/base.ts b/tests/e2e/fixtures/base.ts index 01b4f22a..fd6fc6bb 100644 --- a/tests/e2e/fixtures/base.ts +++ b/tests/e2e/fixtures/base.ts @@ -82,14 +82,19 @@ const defaultMessage = ( expected: unknown, matcherResult?: { actual: unknown }, ) => { - return () => - `${thisType.utils.matcherHint(assertionName, undefined, undefined, { - isNot: thisType.isNot, - })}\n\nExpected:${pass ? '' : ' not '}${thisType.utils.printExpected(expected)}\n${ - matcherResult - ? `Received: ${thisType.utils.printReceived(matcherResult.actual)}` - : '' - }`; + return () => { + const hint = thisType.utils.matcherHint( + assertionName, + undefined, + undefined, + { isNot: thisType.isNot }, + ); + const expectedPart = `Expected:${pass ? '' : ' not '}${thisType.utils.printExpected(expected)}`; + const receivedPart = matcherResult + ? `Received: ${thisType.utils.printReceived(matcherResult.actual)}` + : ''; + return `${hint}\n\n${expectedPart}\n${receivedPart}`; + }; }; export const expect = test.expect.extend({ From 8c4e1db2ec44d5b31a45f282abe3ba348ec6dbdf Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:08:48 +0530 Subject: [PATCH 4/5] nit: rename vars to they take less number of lines --- tests/e2e/fixtures/base.ts | 54 +++++++++++++------------------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/tests/e2e/fixtures/base.ts b/tests/e2e/fixtures/base.ts index fd6fc6bb..fe2c16f0 100644 --- a/tests/e2e/fixtures/base.ts +++ b/tests/e2e/fixtures/base.ts @@ -99,10 +99,10 @@ const defaultMessage = ( export const expect = test.expect.extend({ async toHaveStorage(background: Background, expected: DeepPartial) { - const assertionName = 'toHaveStorage'; + const name = 'toHaveStorage'; let pass: boolean; - let matcherResult: any; + let result: any; const storedData = await getStorage( background, @@ -112,22 +112,16 @@ export const expect = test.expect.extend({ test.expect(storedData).toMatchObject(expected); pass = true; } catch { - matcherResult = { actual: storedData }; + result = { actual: storedData }; pass = false; } return { - name: assertionName, + name: name, pass, expected, - actual: matcherResult?.actual, - message: defaultMessage( - this, - assertionName, - pass, - expected, - matcherResult, - ), + actual: result?.actual, + message: defaultMessage(this, name, pass, expected, result), }; }, @@ -136,10 +130,10 @@ export const expect = test.expect.extend({ expected: number, { timeout = 5000, wait = 1000 }: { timeout?: number; wait?: number } = {}, ) { - const assertionName = 'toHaveBeenCalledTimes'; + const name = 'toHaveBeenCalledTimes'; let pass: boolean; - let matcherResult: { actual: number } | undefined; + let result: { actual: number } | undefined; await sleep(wait); let remainingTime = timeout; @@ -149,7 +143,7 @@ export const expect = test.expect.extend({ pass = true; break; } catch { - matcherResult = { actual: fn.callCount }; + result = { actual: fn.callCount }; pass = false; remainingTime -= 500; await sleep(500); @@ -157,17 +151,11 @@ export const expect = test.expect.extend({ } while (remainingTime > 0); return { - name: assertionName, + name: name, pass, expected, - actual: matcherResult?.actual, - message: defaultMessage( - this, - assertionName, - pass, - expected, - matcherResult, - ), + actual: result?.actual, + message: defaultMessage(this, name, pass, expected, result), }; }, @@ -176,10 +164,10 @@ export const expect = test.expect.extend({ expected: Record, { timeout = 5000, wait = 1000 }: { timeout?: number; wait?: number } = {}, ) { - const assertionName = 'toHaveBeenLastCalledWithMatching'; + const name = 'toHaveBeenLastCalledWithMatching'; let pass: boolean; - let matcherResult: { actual: unknown } | undefined; + let result: { actual: unknown } | undefined; await sleep(wait); let remainingTime = timeout; @@ -191,7 +179,7 @@ export const expect = test.expect.extend({ pass = true; break; } catch { - matcherResult = { actual: fn.calls[fn.calls.length - 1]?.[0] }; + result = { actual: fn.calls[fn.calls.length - 1]?.[0] }; pass = false; remainingTime -= 500; await sleep(500); @@ -199,17 +187,11 @@ export const expect = test.expect.extend({ } while (remainingTime > 0); return { - name: assertionName, + name: name, pass, expected, - actual: matcherResult?.actual, - message: defaultMessage( - this, - assertionName, - pass, - expected, - matcherResult, - ), + actual: result?.actual, + message: defaultMessage(this, name, pass, expected, result), }; }, }); From 7eb066b068f0aec08a79ecc6a3566dc22e24deae Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:09:42 +0530 Subject: [PATCH 5/5] nit --- tests/e2e/fixtures/base.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/fixtures/base.ts b/tests/e2e/fixtures/base.ts index fe2c16f0..117f651d 100644 --- a/tests/e2e/fixtures/base.ts +++ b/tests/e2e/fixtures/base.ts @@ -117,7 +117,7 @@ export const expect = test.expect.extend({ } return { - name: name, + name, pass, expected, actual: result?.actual, @@ -151,7 +151,7 @@ export const expect = test.expect.extend({ } while (remainingTime > 0); return { - name: name, + name, pass, expected, actual: result?.actual, @@ -187,7 +187,7 @@ export const expect = test.expect.extend({ } while (remainingTime > 0); return { - name: name, + name, pass, expected, actual: result?.actual,