Skip to content

Commit 8e39dfc

Browse files
committed
test(remix): Add Shopify Hydrogen E2E test app.
1 parent 831e439 commit 8e39dfc

32 files changed

+1828
-0
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,7 @@ jobs:
10581058
'react-router-6-use-routes',
10591059
'react-router-5',
10601060
'react-router-6',
1061+
'remix-hydrogen',
10611062
'solid',
10621063
'svelte-5',
10631064
'sveltekit',
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
build
2+
node_modules
3+
bin
4+
*.d.ts
5+
dist
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @type {import("@types/eslint").Linter.BaseConfig}
3+
*/
4+
module.exports = {
5+
extends: [
6+
'@remix-run/eslint-config',
7+
'plugin:hydrogen/recommended',
8+
'plugin:hydrogen/typescript',
9+
],
10+
rules: {
11+
'@typescript-eslint/ban-ts-comment': 'off',
12+
'@typescript-eslint/naming-convention': 'off',
13+
'hydrogen/prefer-image-component': 'off',
14+
'no-useless-escape': 'off',
15+
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
16+
'no-case-declarations': 'off',
17+
'no-console': ['warn', {allow: ['warn', 'error']}],
18+
},
19+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
node_modules
2+
/.cache
3+
/build
4+
/dist
5+
/public/build
6+
/.mf
7+
.env
8+
.shopify
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {RemixBrowser, useLocation, useMatches} from '@remix-run/react';
2+
import * as Sentry from '@sentry/remix';
3+
import {StrictMode, startTransition} from 'react';
4+
import {useEffect} from 'react';
5+
import {hydrateRoot} from 'react-dom/client';
6+
7+
Sentry.init({
8+
dsn: window.ENV.SENTRY_DSN,
9+
integrations: [
10+
new Sentry.BrowserTracing({
11+
routingInstrumentation: Sentry.remixRouterInstrumentation(
12+
useEffect,
13+
useLocation,
14+
useMatches,
15+
),
16+
}),
17+
// Replay is only available in the client
18+
new Sentry.Replay(),
19+
new Sentry.BrowserProfilingIntegration(),
20+
],
21+
22+
// Set tracesSampleRate to 1.0 to capture 100%
23+
// of transactions for performance monitoring.
24+
// We recommend adjusting this value in production
25+
tracesSampleRate: 1.0,
26+
27+
// Set `tracePropagationTargets` to control for which URLs distributed tracing should be enabled
28+
tracePropagationTargets: ['localhost', /^https:\/\/yourserver\.io\/api/],
29+
30+
// Capture Replay for 10% of all sessions,
31+
// plus for 100% of sessions with an error
32+
replaysSessionSampleRate: 0.1,
33+
replaysOnErrorSampleRate: 1.0,
34+
35+
// Capture all profiles
36+
profilesSampleRate: 1.0,
37+
});
38+
39+
Sentry.addEventProcessor((event) => {
40+
if (
41+
event.type === 'transaction' &&
42+
(event.contexts?.trace?.op === 'pageload' ||
43+
event.contexts?.trace?.op === 'navigation')
44+
) {
45+
const eventId = event.event_id;
46+
if (eventId) {
47+
window.recordedTransactions = window.recordedTransactions || [];
48+
window.recordedTransactions.push(eventId);
49+
}
50+
}
51+
52+
return event;
53+
});
54+
55+
startTransition(() => {
56+
hydrateRoot(
57+
document,
58+
<StrictMode>
59+
<RemixBrowser />
60+
</StrictMode>,
61+
);
62+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as Sentry from '@sentry/remix';
2+
3+
import {RemixServer} from '@remix-run/react';
4+
import {createContentSecurityPolicy} from '@shopify/hydrogen';
5+
import type {DataFunctionArgs, EntryContext} from '@shopify/remix-oxygen';
6+
import isbot from 'isbot';
7+
import {renderToReadableStream} from 'react-dom/server';
8+
9+
export async function handleError(
10+
error: unknown,
11+
{request}: DataFunctionArgs,
12+
): Promise<void> {
13+
Sentry.captureRemixServerException(error, 'remix.server', request, true);
14+
}
15+
16+
export default async function handleRequest(
17+
request: Request,
18+
responseStatusCode: number,
19+
responseHeaders: Headers,
20+
remixContext: EntryContext,
21+
) {
22+
const {nonce, header, NonceProvider} = createContentSecurityPolicy();
23+
24+
const body = await renderToReadableStream(
25+
<NonceProvider>
26+
<RemixServer context={remixContext} url={request.url} />
27+
</NonceProvider>,
28+
{
29+
nonce,
30+
signal: request.signal,
31+
onError(error) {
32+
// eslint-disable-next-line no-console
33+
console.error(error);
34+
responseStatusCode = 500;
35+
},
36+
},
37+
);
38+
39+
if (isbot(request.headers.get('user-agent'))) {
40+
await body.allReady;
41+
}
42+
43+
responseHeaders.set('Content-Type', 'text/html');
44+
responseHeaders.set('Content-Security-Policy', header);
45+
46+
// Add the document policy header to enable JS profiling
47+
// This is required for Sentry's profiling integration
48+
responseHeaders.set('Document-Policy', 'js-profiling');
49+
50+
return new Response(body, {
51+
headers: responseHeaders,
52+
status: responseStatusCode,
53+
});
54+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// NOTE: https://shopify.dev/docs/api/storefront/latest/queries/cart
2+
export const CART_QUERY_FRAGMENT = `#graphql
3+
fragment Money on MoneyV2 {
4+
currencyCode
5+
amount
6+
}
7+
fragment CartLine on CartLine {
8+
id
9+
quantity
10+
attributes {
11+
key
12+
value
13+
}
14+
cost {
15+
totalAmount {
16+
...Money
17+
}
18+
amountPerQuantity {
19+
...Money
20+
}
21+
compareAtAmountPerQuantity {
22+
...Money
23+
}
24+
}
25+
merchandise {
26+
... on ProductVariant {
27+
id
28+
availableForSale
29+
compareAtPrice {
30+
...Money
31+
}
32+
price {
33+
...Money
34+
}
35+
requiresShipping
36+
title
37+
image {
38+
id
39+
url
40+
altText
41+
width
42+
height
43+
44+
}
45+
product {
46+
handle
47+
title
48+
id
49+
vendor
50+
}
51+
selectedOptions {
52+
name
53+
value
54+
}
55+
}
56+
}
57+
}
58+
fragment CartApiQuery on Cart {
59+
updatedAt
60+
id
61+
checkoutUrl
62+
totalQuantity
63+
buyerIdentity {
64+
countryCode
65+
customer {
66+
id
67+
email
68+
firstName
69+
lastName
70+
displayName
71+
}
72+
email
73+
phone
74+
}
75+
lines(first: $numCartLines) {
76+
nodes {
77+
...CartLine
78+
}
79+
}
80+
cost {
81+
subtotalAmount {
82+
...Money
83+
}
84+
totalAmount {
85+
...Money
86+
}
87+
totalDutyAmount {
88+
...Money
89+
}
90+
totalTaxAmount {
91+
...Money
92+
}
93+
}
94+
note
95+
attributes {
96+
key
97+
value
98+
}
99+
discountCodes {
100+
code
101+
applicable
102+
}
103+
}
104+
` as const;
105+
106+
const MENU_FRAGMENT = `#graphql
107+
fragment MenuItem on MenuItem {
108+
id
109+
resourceId
110+
tags
111+
title
112+
type
113+
url
114+
}
115+
fragment ChildMenuItem on MenuItem {
116+
...MenuItem
117+
}
118+
fragment ParentMenuItem on MenuItem {
119+
...MenuItem
120+
items {
121+
...ChildMenuItem
122+
}
123+
}
124+
fragment Menu on Menu {
125+
id
126+
items {
127+
...ParentMenuItem
128+
}
129+
}
130+
` as const;
131+
132+
export const HEADER_QUERY = `#graphql
133+
fragment Shop on Shop {
134+
id
135+
name
136+
description
137+
primaryDomain {
138+
url
139+
}
140+
brand {
141+
logo {
142+
image {
143+
url
144+
}
145+
}
146+
}
147+
}
148+
query Header(
149+
$country: CountryCode
150+
$headerMenuHandle: String!
151+
$language: LanguageCode
152+
) @inContext(language: $language, country: $country) {
153+
shop {
154+
...Shop
155+
}
156+
menu(handle: $headerMenuHandle) {
157+
...Menu
158+
}
159+
}
160+
${MENU_FRAGMENT}
161+
` as const;
162+
163+
export const FOOTER_QUERY = `#graphql
164+
query Footer(
165+
$country: CountryCode
166+
$footerMenuHandle: String!
167+
$language: LanguageCode
168+
) @inContext(language: $language, country: $country) {
169+
menu(handle: $footerMenuHandle) {
170+
...Menu
171+
}
172+
}
173+
${MENU_FRAGMENT}
174+
` as const;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type {
2+
PredictiveQueryFragment,
3+
SearchProductFragment,
4+
PredictiveProductFragment,
5+
PredictiveCollectionFragment,
6+
PredictivePageFragment,
7+
PredictiveArticleFragment,
8+
} from 'storefrontapi.generated';
9+
10+
export function applyTrackingParams(
11+
resource:
12+
| PredictiveQueryFragment
13+
| SearchProductFragment
14+
| PredictiveProductFragment
15+
| PredictiveCollectionFragment
16+
| PredictiveArticleFragment
17+
| PredictivePageFragment,
18+
params?: string,
19+
) {
20+
if (params) {
21+
return resource?.trackingParameters
22+
? `?${params}&${resource.trackingParameters}`
23+
: `?${params}`;
24+
} else {
25+
return resource?.trackingParameters
26+
? `?${resource.trackingParameters}`
27+
: '';
28+
}
29+
}

0 commit comments

Comments
 (0)