Skip to content

Commit 71babdd

Browse files
committed
Add client-side boilerplate with a simple Playwright test.
1 parent 05438d0 commit 71babdd

File tree

8 files changed

+228
-2
lines changed

8 files changed

+228
-2
lines changed

.github/workflows/build.yml

+1
Original file line numberDiff line numberDiff line change
@@ -605,4 +605,5 @@ jobs:
605605
NODE_VERSION: ${{ matrix.node }}
606606
run: |
607607
cd packages/remix
608+
yarn run playwright install-deps webkit
608609
yarn test:integration

packages/remix/test/integration/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ node_modules
44
/build
55
/public/build
66
.env
7+
/test-results/
8+
/playwright-report/
9+
/playwright/.cache/

packages/remix/test/integration/jest.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ module.exports = {
44
globalSetup: '<rootDir>/test/server/utils/test-setup.ts',
55
globalTeardown: '<rootDir>/test/server/utils/test-teardown.ts',
66
...baseConfig,
7-
testMatch: ['**/*.test.ts'],
7+
testMatch: ['<rootDir>/test/server/**/*.test.ts'],
88
};

packages/remix/test/integration/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dev": "remix dev",
77
"start": "remix-serve build",
88
"test": "yarn build && yarn test:client && yarn test:server",
9-
"test:client": "echo \"TODO\"",
9+
"test:client": "playwright test",
1010
"test:server": "jest"
1111
},
1212
"dependencies": {
@@ -19,6 +19,7 @@
1919
"react-dom": "^17.0.2"
2020
},
2121
"devDependencies": {
22+
"@playwright/test": "^1.24.0",
2223
"@remix-run/dev": "^1.6.5",
2324
"@types/react": "^17.0.47",
2425
"@types/react-dom": "^17.0.17",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { PlaywrightTestConfig } from '@playwright/test';
2+
import { devices } from '@playwright/test';
3+
4+
/**
5+
* See https://playwright.dev/docs/test-configuration.
6+
*/
7+
const config: PlaywrightTestConfig = {
8+
testDir: './test/client',
9+
/* Maximum time one test can run for. */
10+
timeout: 30 * 1000,
11+
expect: {
12+
/**
13+
* Maximum time expect() should wait for the condition to be met.
14+
* For example in `await expect(locator).toHaveText();`
15+
*/
16+
timeout: 5000,
17+
},
18+
/* Run tests in files in parallel */
19+
fullyParallel: true,
20+
/* Fail the build on CI if you accidentally left test.only in the source code. */
21+
forbidOnly: !!process.env.CI,
22+
/* Retry on CI only */
23+
retries: process.env.CI ? 2 : 0,
24+
/* Opt out of parallel tests on CI. */
25+
workers: process.env.CI ? 1 : undefined,
26+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
27+
reporter: 'html',
28+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
29+
use: {
30+
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
31+
actionTimeout: 0,
32+
/* Base URL to use in actions like `await page.goto('/')`. */
33+
// baseURL: 'http://localhost:3000',
34+
35+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
36+
trace: 'on-first-retry',
37+
},
38+
39+
/* Configure projects for major browsers */
40+
projects: [
41+
{
42+
name: 'chromium',
43+
use: {
44+
...devices['Desktop Chrome'],
45+
},
46+
},
47+
48+
{
49+
name: 'firefox',
50+
use: {
51+
...devices['Desktop Firefox'],
52+
},
53+
},
54+
55+
{
56+
name: 'webkit',
57+
use: {
58+
...devices['Desktop Safari'],
59+
},
60+
},
61+
],
62+
63+
webServer: {
64+
command: 'yarn build && yarn start',
65+
port: 3000,
66+
},
67+
};
68+
69+
export default config;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getFirstSentryEnvelopeRequest } from './utils/helpers';
2+
import { test, expect } from '@playwright/test';
3+
import { Event } from '@sentry/types';
4+
5+
test('should add `pageload` transaction on load.', async ({ page }) => {
6+
const envelope = await getFirstSentryEnvelopeRequest<Event>(page, 'http://localhost:3000');
7+
8+
expect(envelope.contexts?.trace.op).toBe('pageload');
9+
expect(envelope.tags?.['routing.instrumentation']).toBe('remix-router');
10+
expect(envelope.type).toBe('transaction');
11+
expect(envelope.transaction).toBe('routes/index');
12+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copied from browser integration tests
2+
import { Page, Request } from '@playwright/test';
3+
import { Event, EventEnvelopeHeaders } from '@sentry/types';
4+
5+
const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//;
6+
7+
const envelopeRequestParser = (request: Request | null): Event => {
8+
// https://develop.sentry.dev/sdk/envelopes/
9+
const envelope = request?.postData() || '';
10+
11+
// Third row of the envelop is the event payload.
12+
return envelope.split('\n').map(line => JSON.parse(line))[2];
13+
};
14+
15+
export const envelopeHeaderRequestParser = (request: Request | null): EventEnvelopeHeaders => {
16+
// https://develop.sentry.dev/sdk/envelopes/
17+
const envelope = request?.postData() || '';
18+
19+
// First row of the envelop is the event payload.
20+
return envelope.split('\n').map(line => JSON.parse(line))[0];
21+
};
22+
23+
/**
24+
* Get Sentry events at the given URL, or the current page.
25+
*
26+
* @param {Page} page
27+
* @param {string} [url]
28+
* @return {*} {Promise<Array<Event>>}
29+
*/
30+
async function getSentryEvents(page: Page, url?: string): Promise<Array<Event>> {
31+
if (url) {
32+
await page.goto(url);
33+
}
34+
const eventsHandle = await page.evaluateHandle<Array<Event>>('window.events');
35+
36+
return eventsHandle.jsonValue();
37+
}
38+
39+
/**
40+
* Waits until a number of requests matching urlRgx at the given URL arrive.
41+
* If the timout option is configured, this function will abort waiting, even if it hasn't reveived the configured
42+
* amount of requests, and returns all the events recieved up to that point in time.
43+
*/
44+
async function getMultipleRequests(
45+
page: Page,
46+
count: number,
47+
urlRgx: RegExp,
48+
requestParser: (req: Request) => Event,
49+
options?: {
50+
url?: string;
51+
timeout?: number;
52+
},
53+
): Promise<Event[]> {
54+
const requests: Promise<Event[]> = new Promise((resolve, reject) => {
55+
let reqCount = count;
56+
const requestData: Event[] = [];
57+
let timeoutId: NodeJS.Timeout | undefined = undefined;
58+
59+
function requestHandler(request: Request): void {
60+
if (urlRgx.test(request.url())) {
61+
try {
62+
reqCount -= 1;
63+
requestData.push(requestParser(request));
64+
65+
if (reqCount === 0) {
66+
if (timeoutId) {
67+
clearTimeout(timeoutId);
68+
}
69+
page.off('request', requestHandler);
70+
resolve(requestData);
71+
}
72+
} catch (err) {
73+
reject(err);
74+
}
75+
}
76+
}
77+
78+
page.on('request', requestHandler);
79+
80+
if (options?.timeout) {
81+
timeoutId = setTimeout(() => {
82+
resolve(requestData);
83+
}, options.timeout);
84+
}
85+
});
86+
87+
if (options?.url) {
88+
await page.goto(options.url);
89+
}
90+
91+
return requests;
92+
}
93+
94+
/**
95+
* Wait and get multiple envelope requests at the given URL, or the current page
96+
*/
97+
async function getMultipleSentryEnvelopeRequests<T>(
98+
page: Page,
99+
count: number,
100+
options?: {
101+
url?: string;
102+
timeout?: number;
103+
},
104+
requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T,
105+
): Promise<T[]> {
106+
// TODO: This is not currently checking the type of envelope, just casting for now.
107+
// We can update this to include optional type-guarding when we have types for Envelope.
108+
return getMultipleRequests(page, count, envelopeUrlRegex, requestParser, options) as Promise<T[]>;
109+
}
110+
111+
/**
112+
* Wait and get the first envelope request at the given URL, or the current page
113+
*
114+
* @template T
115+
* @param {Page} page
116+
* @param {string} [url]
117+
* @return {*} {Promise<T>}
118+
*/
119+
async function getFirstSentryEnvelopeRequest<T>(
120+
page: Page,
121+
url?: string,
122+
requestParser: (req: Request) => T = envelopeRequestParser as (req: Request) => T,
123+
): Promise<T> {
124+
return (await getMultipleSentryEnvelopeRequests<T>(page, 1, { url }, requestParser))[0];
125+
}
126+
127+
export { getMultipleSentryEnvelopeRequests, getFirstSentryEnvelopeRequest, getSentryEvents };

packages/remix/test/integration/yarn.lock

+13
Original file line numberDiff line numberDiff line change
@@ -1091,6 +1091,14 @@
10911091
dependencies:
10921092
json-parse-even-better-errors "^2.3.1"
10931093

1094+
"@playwright/test@^1.24.0":
1095+
version "1.24.0"
1096+
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.24.0.tgz#bef07eddeebba21358ab15e9b0b7a79df0479b76"
1097+
integrity sha512-sZLH2N6aWN9TtG+vMjNSomSfX0dSVHwWE+GhHQPV+ZeGcuZ/6CgMCGFuGjobgq/hNF9ZkuVOjeyoceZ0owKnHQ==
1098+
dependencies:
1099+
"@types/node" "*"
1100+
playwright-core "1.24.0"
1101+
10941102
"@remix-run/dev@^1.6.5":
10951103
version "1.6.5"
10961104
resolved "https://registry.yarnpkg.com/@remix-run/dev/-/dev-1.6.5.tgz#dfa4e58288f79ed7616b63698ee8ad4cf6b8fe60"
@@ -5025,6 +5033,11 @@ pkg-dir@^3.0.0:
50255033
dependencies:
50265034
find-up "^3.0.0"
50275035

5036+
5037+
version "1.24.0"
5038+
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.24.0.tgz#ece821ceaf241bb7206329b1242b0cf6c1c60bdb"
5039+
integrity sha512-BkDWdVsoEEC8m2glQlfNu1EN2qvjBsLIg5bD0wjrfwv9zVHktIsp80yYFObAcWreLNYhfRP4PlXE04lr5R4DFQ==
5040+
50285041
posix-character-classes@^0.1.0:
50295042
version "0.1.1"
50305043
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"

0 commit comments

Comments
 (0)