@salesforce/pwa-kit-react-sdk 3.8.0-preview.0-basepath → 3.8.0-preview.2-basepath

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/package.json +6 -5
  3. package/ssr/browser/main.js +136 -0
  4. package/ssr/browser/main.test.js +54 -0
  5. package/ssr/server/react-rendering.js +448 -0
  6. package/ssr/server/react-rendering.test.js +786 -0
  7. package/ssr/universal/compatibility.js +31 -0
  8. package/ssr/universal/components/_app/index.js +35 -0
  9. package/ssr/universal/components/_app/index.test.js +20 -0
  10. package/ssr/universal/components/_app-config/index.js +104 -0
  11. package/ssr/universal/components/_app-config/index.test.js +22 -0
  12. package/ssr/universal/components/_document/index.js +92 -0
  13. package/ssr/universal/components/_document/index.test.js +58 -0
  14. package/ssr/universal/components/_error/index.js +55 -0
  15. package/ssr/universal/components/_error/index.test.js +28 -0
  16. package/ssr/universal/components/app-error-boundary/index.js +113 -0
  17. package/ssr/universal/components/app-error-boundary/index.test.js +109 -0
  18. package/ssr/universal/components/fetch-strategy/index.js +42 -0
  19. package/ssr/universal/components/refresh/index.js +123 -0
  20. package/ssr/universal/components/refresh/index.test.js +78 -0
  21. package/ssr/universal/components/route-component/index.js +415 -0
  22. package/ssr/universal/components/route-component/index.test.js +378 -0
  23. package/ssr/universal/components/switch/index.js +62 -0
  24. package/ssr/universal/components/throw-404/index.js +36 -0
  25. package/ssr/universal/components/throw-404/index.test.js +26 -0
  26. package/ssr/universal/components/with-correlation-id/index.js +36 -0
  27. package/ssr/universal/components/with-legacy-get-props/index.js +100 -0
  28. package/ssr/universal/components/with-legacy-get-props/index.test.js +35 -0
  29. package/ssr/universal/components/with-react-query/index.js +130 -0
  30. package/ssr/universal/components/with-react-query/index.test.js +101 -0
  31. package/ssr/universal/contexts/index.js +72 -0
  32. package/ssr/universal/contexts/index.test.js +101 -0
  33. package/ssr/universal/errors.js +34 -0
  34. package/ssr/universal/errors.test.js +20 -0
  35. package/ssr/universal/events.js +38 -0
  36. package/ssr/universal/events.test.js +39 -0
  37. package/ssr/universal/hooks/index.js +52 -0
  38. package/ssr/universal/routes.js +15 -0
  39. package/ssr/universal/utils.client.test.js +46 -0
  40. package/ssr/universal/utils.js +61 -0
  41. package/ssr/universal/utils.server.test.js +24 -0
  42. package/utils/assets.js +120 -0
  43. package/utils/assets.test.js +106 -0
  44. package/utils/logger-instance.js +19 -0
  45. package/utils/performance.js +126 -0
  46. package/utils/performance.test.js +50 -0
  47. package/utils/url.js +39 -0
  48. package/utils/url.test.js +47 -0
  49. package/utils/uuidv4.client.js +21 -0
  50. package/utils/uuidv4.client.test.js +27 -0
  51. package/utils/warnings.js +81 -0
  52. package/utils/warnings.test.js +48 -0
@@ -0,0 +1,786 @@
1
+ "use strict";
2
+
3
+ var _reactRendering = require("./react-rendering");
4
+ var _crypto = require("crypto");
5
+ var _buildRemoteServer = require("@salesforce/pwa-kit-runtime/ssr/server/build-remote-server");
6
+ var _supertest = _interopRequireDefault(require("supertest"));
7
+ var _nodeHtmlParser = require("node-html-parser");
8
+ var _path = _interopRequireDefault(require("path"));
9
+ var _ssrServer = require("@salesforce/pwa-kit-runtime/utils/ssr-server");
10
+ var _compatibility = require("../universal/compatibility");
11
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
13
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
14
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
15
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
16
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
17
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
18
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /**
19
+ * @jest-environment node
20
+ */ /*
21
+ * Copyright (c) 2023, Salesforce, Inc.
22
+ * All rights reserved.
23
+ * SPDX-License-Identifier: BSD-3-Clause
24
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
25
+ */ // The @jest-environment comment block *MUST* be the first line of the file for the tests to pass.
26
+ // That conflicts with the monorepo header rule, so we must disable the rule!
27
+ /* eslint-disable header/header */
28
+ const opts = (overrides = {}) => {
29
+ const fixtures = _path.default.join(__dirname, '..', '..', 'ssr', 'server', 'test_fixtures');
30
+ const defaults = {
31
+ buildDir: fixtures,
32
+ mobify: {
33
+ ssrEnabled: true,
34
+ ssrParameters: {
35
+ proxyConfigs: []
36
+ }
37
+ },
38
+ protocol: 'http'
39
+ };
40
+ return _objectSpread(_objectSpread({}, defaults), overrides);
41
+ };
42
+ jest.mock('../universal/compatibility', () => {
43
+ const AppConfig = jest.requireActual('../universal/components/_app-config').default;
44
+ const {
45
+ withReactQuery
46
+ } = jest.requireActual('../universal/components/with-react-query');
47
+ const {
48
+ withLegacyGetProps
49
+ } = jest.requireActual('../universal/components/with-legacy-get-props');
50
+ const appConfig = withReactQuery(withLegacyGetProps(AppConfig));
51
+ return {
52
+ getAppConfig: () => appConfig
53
+ };
54
+ });
55
+ jest.mock('../universal/routes', () => {
56
+ // TODO: Can these requires be converted to top-level imports?
57
+ /* eslint-disable @typescript-eslint/no-var-requires */
58
+ const React = require('react');
59
+ const PropTypes = require('prop-types');
60
+ const errors = require('../universal/errors');
61
+ const {
62
+ Redirect,
63
+ Link
64
+ } = require('react-router-dom');
65
+ const {
66
+ Helmet
67
+ } = require('react-helmet');
68
+ const {
69
+ useQuery
70
+ } = require('@tanstack/react-query');
71
+ const {
72
+ useServerContext
73
+ } = require('../universal/hooks');
74
+ /* eslint-enable @typescript-eslint/no-var-requires */
75
+
76
+ // Test utility to exercise paths that work with @loadable/component.
77
+ const fakeLoadable = Wrapped => {
78
+ class FakeLoadable extends React.Component {
79
+ static preload() {
80
+ return Promise.resolve(Wrapped);
81
+ }
82
+ render() {
83
+ return /*#__PURE__*/React.createElement(Wrapped, null);
84
+ }
85
+ }
86
+ return FakeLoadable;
87
+ };
88
+ class PWAPage extends React.Component {
89
+ static getProps() {
90
+ return Promise.resolve();
91
+ }
92
+ static getTemplateName() {
93
+ return 'templateName';
94
+ }
95
+ render() {
96
+ return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", null, "This is a PWA"), /*#__PURE__*/React.createElement(Link, {
97
+ to: "/link"
98
+ }, "This is a link (for Router basepath testing)"));
99
+ }
100
+ }
101
+ class UnknownErrorPage extends React.Component {
102
+ static getProps() {
103
+ throw new Error('This is an error');
104
+ }
105
+ render() {
106
+ return /*#__PURE__*/React.createElement("div", null, "This should not be rendered");
107
+ }
108
+ }
109
+ class ThrowStringErrorPage extends React.Component {
110
+ static getProps() {
111
+ throw 'This is an error';
112
+ }
113
+ render() {
114
+ return /*#__PURE__*/React.createElement("div", null, "This should not be rendered");
115
+ }
116
+ }
117
+ class KnownErrorPage extends React.Component {
118
+ static getProps() {
119
+ throw new errors.HTTPError(503, 'Service not available');
120
+ }
121
+ render() {
122
+ return /*#__PURE__*/React.createElement("div", null, "This should not be rendered");
123
+ }
124
+ }
125
+ class GetProps404ErrorPage extends React.Component {
126
+ static getProps() {
127
+ throw new errors.HTTPNotFound('Not found');
128
+ }
129
+ render() {
130
+ return /*#__PURE__*/React.createElement("div", null, "This should not be rendered");
131
+ }
132
+ }
133
+ class InitSetsStatusPage extends React.Component {
134
+ static getProps({
135
+ res
136
+ }) {
137
+ res.status(418);
138
+ return Promise.resolve();
139
+ }
140
+ render() {
141
+ return /*#__PURE__*/React.createElement("div", null, "418 - I am a Teapot");
142
+ }
143
+ }
144
+ class GetPropsRejectsWithEmptyString extends React.Component {
145
+ static getProps() {
146
+ return Promise.reject('');
147
+ }
148
+ render() {
149
+ return /*#__PURE__*/React.createElement("div", null, "This should not be rendered");
150
+ }
151
+ }
152
+ class RenderThrowsError extends React.Component {
153
+ static getProps() {
154
+ return Promise.resolve();
155
+ }
156
+ // eslint-disable-next-line react/require-render-return
157
+ render() {
158
+ throw new Error('This is an error rendering');
159
+ }
160
+ }
161
+ class GetPropsReturnsObject extends React.Component {
162
+ static getProps() {
163
+ return {
164
+ prop: 'prop-value'
165
+ };
166
+ }
167
+ render() {
168
+ return /*#__PURE__*/React.createElement("div", null, this.props.prop);
169
+ }
170
+ }
171
+ class RedirectPage extends React.Component {
172
+ static getProps() {
173
+ return Promise.resolve();
174
+ }
175
+ render() {
176
+ return /*#__PURE__*/React.createElement(Redirect, {
177
+ to: "/elsewhere/"
178
+ });
179
+ }
180
+ }
181
+ class HelmetPage extends React.Component {
182
+ static getProps() {
183
+ return Promise.resolve();
184
+ }
185
+ static getTemplateName() {
186
+ return 'templateName';
187
+ }
188
+ render() {
189
+ return /*#__PURE__*/React.createElement(Helmet, null, /*#__PURE__*/React.createElement("html", {
190
+ lang: "helmet-html-attribute"
191
+ }), /*#__PURE__*/React.createElement("body", {
192
+ className: "helmet-body-attribute"
193
+ }), /*#__PURE__*/React.createElement("title", null, "Helmet title"), /*#__PURE__*/React.createElement("base", {
194
+ target: "_blank",
195
+ href: "http://mysite.com/"
196
+ }), /*#__PURE__*/React.createElement("meta", {
197
+ name: "helmet-meta-1",
198
+ content: "helmet-meta-1"
199
+ }), /*#__PURE__*/React.createElement("meta", {
200
+ property: "helmet-meta-2",
201
+ content: "helmet-meta-2"
202
+ }), /*#__PURE__*/React.createElement("link", {
203
+ rel: "helmet-link-1",
204
+ href: "http://mysite.com/example"
205
+ }), /*#__PURE__*/React.createElement("link", {
206
+ rel: "helmet-link-2",
207
+ href: "http://mysite.com/img/apple-touch-icon-57x57.png"
208
+ }), /*#__PURE__*/React.createElement("script", {
209
+ src: "http://include.com/pathtojs.js",
210
+ type: "text/javascript"
211
+ }), /*#__PURE__*/React.createElement("script", {
212
+ type: "application/ld+json"
213
+ }, `
214
+ {
215
+ "@context": "http://schema.org"
216
+ }
217
+ `), /*#__PURE__*/React.createElement("noscript", null, `
218
+ <link rel="stylesheet" type="text/css" href="foo.css" />
219
+ `), /*#__PURE__*/React.createElement("style", {
220
+ type: "text/css"
221
+ }, `
222
+ body {
223
+ background-color: blue;
224
+ }
225
+ `));
226
+ }
227
+ }
228
+ class XSSPage extends React.Component {
229
+ static getProps() {
230
+ return {
231
+ prop: '<script>alert("hey! give me your money")</script>'
232
+ };
233
+ }
234
+ render() {
235
+ return /*#__PURE__*/React.createElement("div", null, "XSS attack");
236
+ }
237
+ }
238
+ const UseQueryResolvesObject = () => {
239
+ const {
240
+ data,
241
+ isLoading
242
+ } = useQuery(['use-query-resolves-object'], /*#__PURE__*/_asyncToGenerator(function* () {
243
+ return {
244
+ prop: 'prop-value'
245
+ };
246
+ }));
247
+ return /*#__PURE__*/React.createElement("div", null, isLoading ? 'loading' : data.prop);
248
+ };
249
+ const DisabledUseQueryIsntResolved = () => {
250
+ const {
251
+ data,
252
+ isLoading
253
+ } = useQuery(['use-query-resolves-object'], /*#__PURE__*/_asyncToGenerator(function* () {
254
+ return {
255
+ prop: 'prop-value'
256
+ };
257
+ }), {
258
+ enabled: false
259
+ });
260
+ return /*#__PURE__*/React.createElement("div", null, isLoading ? 'loading' : data.prop);
261
+ };
262
+ const GetServerContext = () => {
263
+ const {
264
+ res
265
+ } = useServerContext();
266
+ if (res) {
267
+ console.log('--- isServerSide');
268
+ res.status(404);
269
+ }
270
+ return /*#__PURE__*/React.createElement("div", null);
271
+ };
272
+ GetPropsReturnsObject.propTypes = {
273
+ prop: PropTypes.node
274
+ };
275
+ return {
276
+ __esModule: true,
277
+ default: [{
278
+ path: '/pwa/',
279
+ component: fakeLoadable(PWAPage)
280
+ }, {
281
+ path: '/unknown-error/',
282
+ component: UnknownErrorPage
283
+ }, {
284
+ path: '/throw-string/',
285
+ component: ThrowStringErrorPage
286
+ }, {
287
+ path: '/known-error/',
288
+ component: KnownErrorPage
289
+ }, {
290
+ path: '/404-in-get-props-error/',
291
+ component: GetProps404ErrorPage
292
+ }, {
293
+ path: '/redirect/',
294
+ component: RedirectPage
295
+ }, {
296
+ path: '/init-sets-status/',
297
+ component: InitSetsStatusPage
298
+ }, {
299
+ path: '/get-props-returns-object/',
300
+ component: GetPropsReturnsObject
301
+ }, {
302
+ path: '/get-props-rejects-with-empty-string/',
303
+ component: GetPropsRejectsWithEmptyString
304
+ }, {
305
+ path: '/render-throws-error/',
306
+ component: RenderThrowsError
307
+ }, {
308
+ path: '/render-helmet/',
309
+ component: fakeLoadable(HelmetPage)
310
+ }, {
311
+ path: '/xss/',
312
+ component: XSSPage
313
+ }, {
314
+ path: '/use-query-resolves-object/',
315
+ component: UseQueryResolvesObject
316
+ }, {
317
+ path: '/disabled-use-query-isnt-resolved/',
318
+ component: DisabledUseQueryIsntResolved
319
+ }, {
320
+ path: '/server-context',
321
+ component: GetServerContext
322
+ }]
323
+ };
324
+ });
325
+ jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-server', () => {
326
+ const actual = jest.requireActual('@salesforce/pwa-kit-runtime/utils/ssr-server');
327
+ return _objectSpread(_objectSpread({}, actual), {}, {
328
+ isRemote: jest.fn()
329
+ });
330
+ });
331
+ jest.mock('@loadable/server', () => {
332
+ const lodableServer = jest.requireActual('@loadable/server');
333
+ return _objectSpread(_objectSpread({}, lodableServer), {}, {
334
+ // Tests aren't being run through webpack, therefore no chunks or `loadable-stats.json`
335
+ // file is being created. ChunkExtractor causes a file read exception. For this
336
+ // reason, we mock the implementation to do nothing.
337
+ ChunkExtractor: function () {
338
+ return {
339
+ collectChunks: jest.fn().mockImplementation(x => x),
340
+ getScriptElements: jest.fn().mockReturnValue([])
341
+ };
342
+ }
343
+ });
344
+ });
345
+ jest.mock('@salesforce/pwa-kit-runtime/ssr/server/build-remote-server', () => {
346
+ const actual = jest.requireActual('@salesforce/pwa-kit-runtime/ssr/server/build-remote-server');
347
+ return _objectSpread(_objectSpread({}, actual), {}, {
348
+ RemoteServerFactory: _objectSpread(_objectSpread({}, actual.RemoteServerFactory), {}, {
349
+ _setRequestId: jest.fn()
350
+ })
351
+ });
352
+ });
353
+ describe('The Node SSR Environment', () => {
354
+ const OLD_ENV = process.env;
355
+ beforeAll(() => {
356
+ // These values are not allowed to be `undefined` when `isRemote` returns true. So we mock them.
357
+ process.env.BUNDLE_ID = '1';
358
+ process.env.DEPLOY_TARGET = 'production';
359
+ process.env.EXTERNAL_DOMAIN_NAME = 'test.com';
360
+ process.env.MOBIFY_PROPERTY_ID = 'test';
361
+ });
362
+ afterAll(() => {
363
+ process.env = OLD_ENV; // Restore old environment
364
+ });
365
+ afterEach(() => {
366
+ jest.restoreAllMocks();
367
+ });
368
+
369
+ /**
370
+ * Scripts are "safe" if they are external, not executable or on our allow list of
371
+ * static, inline scripts.
372
+ */
373
+ const scriptsAreSafe = doc => {
374
+ const scripts = Array.from(doc.querySelectorAll('script'));
375
+ expect(scripts.length > 0).toBe(true);
376
+ return scripts.every(script => {
377
+ const external = script.hasAttribute('src');
378
+ const executable = !script.hasAttribute('type') || script.getAttribute('type') === 'application/javascript';
379
+ const allowlisted = _reactRendering.ALLOWLISTED_INLINE_SCRIPTS.indexOf(script.innerHTML) >= 0;
380
+ return external || !executable || allowlisted;
381
+ });
382
+ };
383
+ const dataFromHTML = doc => JSON.parse(doc.querySelector('#mobify-data').innerHTML);
384
+ const cases = [{
385
+ description: `rendering PWA's for desktop`,
386
+ req: {
387
+ url: '/pwa/'
388
+ },
389
+ assertions: res => {
390
+ expect(res.statusCode).toBe(200);
391
+ const html = res.text;
392
+ console.error(html);
393
+ const doc = (0, _nodeHtmlParser.parse)(html);
394
+ const include = ['<div>This is a PWA</div>'];
395
+ const dataScript = doc.querySelectorAll('script[id=mobify-data]')[0];
396
+ expect(dataScript.innerHTML.split(/\r\n|\r|\n/)).toHaveLength(1);
397
+ include.forEach(s => expect(html).toEqual(expect.stringContaining(s)));
398
+ expect(scriptsAreSafe(doc)).toBe(true);
399
+ }
400
+ }, {
401
+ description: `rendering PWA's for tablet`,
402
+ req: {
403
+ url: '/pwa/'
404
+ },
405
+ assertions: res => {
406
+ expect(res.statusCode).toBe(200);
407
+ const html = res.text;
408
+ const doc = (0, _nodeHtmlParser.parse)(html);
409
+ const include = ['<div>This is a PWA</div>'];
410
+ include.forEach(s => expect(html).toEqual(expect.stringContaining(s)));
411
+ expect(scriptsAreSafe(doc)).toBe(true);
412
+ }
413
+ }, {
414
+ description: `rendering PWA's for mobile`,
415
+ req: {
416
+ url: '/pwa/'
417
+ },
418
+ assertions: res => {
419
+ expect(res.statusCode).toBe(200);
420
+ const html = res.text;
421
+ const doc = (0, _nodeHtmlParser.parse)(html);
422
+ const include = ['<div>This is a PWA</div>'];
423
+ include.forEach(s => expect(html).toEqual(expect.stringContaining(s)));
424
+ expect(scriptsAreSafe(doc)).toBe(true);
425
+ }
426
+ }, {
427
+ description: `rendering PWA's in "mobify-server-only" mode should not execute scripts on the client`,
428
+ req: {
429
+ url: '/pwa/',
430
+ query: {
431
+ mobify_server_only: '1'
432
+ }
433
+ },
434
+ assertions: res => {
435
+ const html = res.text;
436
+ const doc = (0, _nodeHtmlParser.parse)(html);
437
+ const include = ['<div>This is a PWA</div>'];
438
+ include.forEach(s => expect(html).toEqual(expect.stringContaining(s)));
439
+ doc.querySelectorAll('script').forEach(script => {
440
+ // application/json prevents execution!
441
+ expect(script.getAttribute('type')).toBe('application/json');
442
+ });
443
+ }
444
+ }, {
445
+ description: `rendering PWA's in "__server-only" mode should not execute scripts on the client`,
446
+ req: {
447
+ url: '/pwa/',
448
+ query: {
449
+ __server_only: '1'
450
+ }
451
+ },
452
+ assertions: res => {
453
+ const html = res.text;
454
+ const doc = (0, _nodeHtmlParser.parse)(html);
455
+ const include = ['<div>This is a PWA</div>'];
456
+ include.forEach(s => expect(html).toEqual(expect.stringContaining(s)));
457
+ doc.querySelectorAll('script').forEach(script => {
458
+ // application/json prevents execution!
459
+ expect(script.getAttribute('type')).toBe('application/json');
460
+ });
461
+ }
462
+ }, {
463
+ description: `rendering PWA's with legacy "mobify_pretty" mode should print stylized global state`,
464
+ req: {
465
+ url: '/pwa/',
466
+ query: {
467
+ mobify_pretty: '1'
468
+ }
469
+ },
470
+ assertions: res => {
471
+ const html = res.text;
472
+ const doc = (0, _nodeHtmlParser.parse)(html);
473
+ const include = ['<div>This is a PWA</div>'];
474
+ include.forEach(s => expect(html).toEqual(expect.stringContaining(s)));
475
+ const script = doc.querySelectorAll('script[id=mobify-data]')[0];
476
+ expect(script.innerHTML.split(/\r\n|\r|\n/).length).toBeGreaterThan(1);
477
+ }
478
+ }, {
479
+ description: `rendering PWA's with "__pretty_print" mode should print stylized global state`,
480
+ req: {
481
+ url: '/pwa/',
482
+ query: {
483
+ __pretty_print: '1'
484
+ }
485
+ },
486
+ assertions: res => {
487
+ const html = res.text;
488
+ const doc = (0, _nodeHtmlParser.parse)(html);
489
+ const include = ['<div>This is a PWA</div>'];
490
+ include.forEach(s => expect(html).toEqual(expect.stringContaining(s)));
491
+ const script = doc.querySelectorAll('script[id=mobify-data]')[0];
492
+ expect(script.innerHTML.split(/\r\n|\r|\n/).length).toBeGreaterThan(1);
493
+ }
494
+ }, {
495
+ description: `404 when no route matches`,
496
+ req: {
497
+ url: '/this-should-404/'
498
+ },
499
+ assertions: res => {
500
+ expect(res.statusCode).toBe(404);
501
+ }
502
+ }, {
503
+ description: `404 when getProps method throws a 404`,
504
+ req: {
505
+ url: '/404-in-get-props-error/'
506
+ },
507
+ assertions: res => {
508
+ expect(res.statusCode).toBe(404);
509
+ }
510
+ }, {
511
+ description: `supports react-routers redirect mechanism`,
512
+ req: {
513
+ url: '/redirect/'
514
+ },
515
+ assertions: res => {
516
+ expect(res.statusCode).toBe(302);
517
+ }
518
+ }, {
519
+ description: `500 on unknown errors in getProps`,
520
+ req: {
521
+ url: '/unknown-error/'
522
+ },
523
+ assertions: res => {
524
+ expect(res.statusCode).toBe(500);
525
+ }
526
+ }, {
527
+ description: `500 when string (not Error) thrown in getProps`,
528
+ req: {
529
+ url: '/throw-string/'
530
+ },
531
+ assertions: res => {
532
+ expect(res.statusCode).toBe(500);
533
+ }
534
+ }, {
535
+ description: `5XX on known HTTP errors in getProps`,
536
+ req: {
537
+ url: '/known-error/'
538
+ },
539
+ assertions: res => {
540
+ expect(res.statusCode).toBe(503);
541
+ }
542
+ }, {
543
+ description: `Respects HTTP status codes set in init() methods`,
544
+ req: {
545
+ url: '/init-sets-status/'
546
+ },
547
+ assertions: res => {
548
+ expect(res.statusCode).toBe(418);
549
+ }
550
+ }, {
551
+ description: `Works if the user returns an Object of props, instead of a Promise`,
552
+ req: {
553
+ url: '/get-props-returns-object/'
554
+ },
555
+ assertions: res => {
556
+ expect(res.statusCode).toBe(200);
557
+ const html = res.text;
558
+ const include = ['<div>prop-value</div>'];
559
+ include.forEach(s => expect(html).toEqual(expect.stringContaining(s)));
560
+ }
561
+ }, {
562
+ description: `Renders the error page if getProps rejects with an empty string`,
563
+ req: {
564
+ url: '/get-props-rejects-with-empty-string/'
565
+ },
566
+ assertions: res => {
567
+ const html = res.text;
568
+ const doc = (0, _nodeHtmlParser.parse)(html);
569
+ const data = dataFromHTML(doc);
570
+ expect(data.__ERROR__.message).toBe('Internal Server Error');
571
+ expect(typeof data.__ERROR__.stack).toEqual((0, _ssrServer.isRemote)() ? 'undefined' : 'string');
572
+ expect(data.__ERROR__.status).toBe(500);
573
+ }
574
+ }, {
575
+ description: `Renders the error page instead if there is an error during component rendering`,
576
+ req: {
577
+ url: '/render-throws-error/'
578
+ },
579
+ assertions: res => {
580
+ const html = res.text;
581
+ const doc = (0, _nodeHtmlParser.parse)(html);
582
+ const data = dataFromHTML(doc);
583
+ expect(data.__ERROR__.message).toBe('Internal Server Error');
584
+ expect(typeof data.__ERROR__.stack).toEqual((0, _ssrServer.isRemote)() ? 'undefined' : 'string');
585
+ expect(data.__ERROR__.status).toBe(500);
586
+ expect(res.statusCode).toBe(500);
587
+ }
588
+ }, {
589
+ description: `Renders react-helmet tags`,
590
+ req: {
591
+ url: '/render-helmet/'
592
+ },
593
+ assertions: res => {
594
+ expect(res.statusCode).toBe(200);
595
+ const html = res.text;
596
+ const doc = (0, _nodeHtmlParser.parse)(html);
597
+ const head = doc.querySelector('head');
598
+ expect(html).toContain('lang="helmet-html-attribute"');
599
+ expect(doc.querySelector('body').getAttribute('class')).toBe('helmet-body-attribute');
600
+ expect(head.querySelector(`title`).innerHTML).toBe('Helmet title');
601
+ expect(head.querySelector('base').getAttribute('target')).toBe('_blank');
602
+ expect(doc.querySelector('style').innerHTML).toContain('background-color: blue;');
603
+ expect(doc.querySelector('noscript').innerHTML).toContain('<link rel="stylesheet" type="text/css" href="foo.css" />');
604
+ expect(doc.querySelector('noscript').innerHTML).toEqual(expect.stringContaining('<link rel="stylesheet" type="text/css" href="foo.css" />'));
605
+ expect(head.querySelector('meta[name="helmet-meta-1"]')).not.toBeNull();
606
+ expect(head.querySelector('meta[property="helmet-meta-2"]')).not.toBeNull();
607
+ expect(head.querySelector('link[rel="helmet-link-1"]')).not.toBeNull();
608
+ expect(head.querySelector('link[rel="helmet-link-2"]')).not.toBeNull();
609
+ expect(head.querySelector('script[src="http://include.com/pathtojs.js"]')).not.toBeNull();
610
+ expect(head.querySelector('script[type="application/ld+json"]').innerHTML).toContain(`"@context": "http://schema.org"`);
611
+ }
612
+ }, {
613
+ description: `Frozen state is escaped preventing injection attacks`,
614
+ req: {
615
+ url: '/xss/'
616
+ },
617
+ assertions: res => {
618
+ const html = res.text;
619
+ const doc = (0, _nodeHtmlParser.parse)(html);
620
+ const scriptContent = doc.querySelector('#mobify-data').innerHTML;
621
+ expect(scriptContent).not.toContain('<script>');
622
+ }
623
+ }, {
624
+ description: `AppConfig errors are caught`,
625
+ req: {
626
+ url: '/pwa/'
627
+ },
628
+ mocks: () => {
629
+ const AppConfig = (0, _compatibility.getAppConfig)();
630
+ jest.spyOn(AppConfig.prototype, 'render').mockImplementation(() => {
631
+ throw new Error();
632
+ });
633
+ },
634
+ assertions: res => {
635
+ const html = res.text;
636
+ expect(res.statusCode).toBe(500);
637
+ const shouldIncludeErrorStack = !(0, _ssrServer.isRemote)();
638
+ expect(html).toContain(shouldIncludeErrorStack ? 'Error: ' : 'Internal Server Error');
639
+ }
640
+ }, {
641
+ description: `AppConfig getBasePath works correctly`,
642
+ req: {
643
+ url: '/base/pwa'
644
+ },
645
+ mocks: () => {
646
+ const AppConfig = (0, _compatibility.getAppConfig)();
647
+ jest.spyOn(AppConfig, 'getBasePath').mockImplementation(() => {
648
+ return '/base';
649
+ });
650
+ },
651
+ assertions: res => {
652
+ expect(res.statusCode).toBe(200);
653
+ const html = res.text;
654
+ const doc = (0, _nodeHtmlParser.parse)(html);
655
+ const links = doc.querySelectorAll('a');
656
+ links.forEach(link => {
657
+ const href = link.getAttribute('href');
658
+ expect(href.startsWith('/base')).toBe(true);
659
+ });
660
+ expect(html).toContain('This is a PWA');
661
+ }
662
+ }, {
663
+ description: `AppConfig getBasePath works correctly when it is empty string`,
664
+ req: {
665
+ url: '/pwa'
666
+ },
667
+ mocks: () => {
668
+ const AppConfig = (0, _compatibility.getAppConfig)();
669
+ jest.spyOn(AppConfig, 'getBasePath').mockImplementation(() => {
670
+ return '';
671
+ });
672
+ },
673
+ assertions: res => {
674
+ expect(res.statusCode).toBe(200);
675
+ const html = res.text;
676
+ expect(html).toContain('This is a PWA');
677
+ }
678
+ }, {
679
+ description: `Works if the user resolves an Object with useQuery`,
680
+ req: {
681
+ url: '/use-query-resolves-object/'
682
+ },
683
+ assertions: res => {
684
+ expect(res.statusCode).toBe(200);
685
+ const html = res.text;
686
+ expect(html).toEqual(expect.stringContaining('<div>prop-value</div>'));
687
+ }
688
+ }, {
689
+ description: `Disabled useQuery queries aren't run on the server`,
690
+ req: {
691
+ url: '/disabled-use-query-isnt-resolved/'
692
+ },
693
+ assertions: res => {
694
+ expect(res.statusCode).toBe(200);
695
+ const html = res.text;
696
+ expect(html).toEqual(expect.stringContaining('<div>loading</div>'));
697
+ }
698
+ }, {
699
+ description: 'Get the server context and set the response status to 404',
700
+ req: {
701
+ url: '/server-context'
702
+ },
703
+ mocks: () => {
704
+ jest.spyOn(console, 'log');
705
+ },
706
+ assertions: res => {
707
+ expect(res.statusCode).toBe(404);
708
+
709
+ // Because of the prepass step we'll expect that this method is called twice.
710
+ expect(console.log).toHaveBeenCalledTimes(2);
711
+ }
712
+ }, {
713
+ description: `Server-Timing header is present in the response`,
714
+ req: {
715
+ url: '/pwa/',
716
+ query: {
717
+ __server_timing: '1'
718
+ }
719
+ },
720
+ assertions: res => {
721
+ expect(res.headers['server-timing']).toContain('route-matching;dur=');
722
+ expect(res.headers['server-timing']).toContain('render-to-string;dur=');
723
+ expect(res.headers['server-timing']).toContain('total;dur=');
724
+ }
725
+ }];
726
+ const isRemoteValues = [true, false];
727
+ _buildRemoteServer.RemoteServerFactory._setRequestId.mockImplementation(_app => {
728
+ _app.use((req, res, next) => {
729
+ res.locals.requestId = (0, _crypto.randomUUID)();
730
+ next();
731
+ });
732
+ });
733
+ isRemoteValues.forEach(isRemoteValue => {
734
+ // Rename `assertions` to `expectations` to pass linter rule
735
+ cases.forEach(({
736
+ description,
737
+ req,
738
+ assertions: expectations,
739
+ mocks
740
+ }) => {
741
+ test(`renders PWA pages properly when ${isRemoteValue ? 'remote' : 'local'} (${description})`, /*#__PURE__*/_asyncToGenerator(function* () {
742
+ // Mock `isRemote` per test execution.
743
+ _ssrServer.isRemote.mockReturnValue(isRemoteValue);
744
+ process.env.NODE_ENV = isRemoteValue ? 'production' : 'development';
745
+ const {
746
+ url,
747
+ headers,
748
+ query
749
+ } = req;
750
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
751
+ app.get('/*', _reactRendering.render);
752
+ if (mocks) {
753
+ mocks();
754
+ }
755
+ const res = yield (0, _supertest.default)(app).get(url).set(headers || {}).query(query || {});
756
+ expectations(res);
757
+ }));
758
+ });
759
+ });
760
+ });
761
+ describe('getLocationSearch', function () {
762
+ test('interprets + sign as space when interpretsPlusSignAsSpace is set to true in config', () => {
763
+ const req = {
764
+ originalUrl: '/hello-word?q=mens+shirt%20dresses',
765
+ query: {
766
+ q: 'mens+shirt%20dresses'
767
+ }
768
+ };
769
+ const output = (0, _reactRendering.getLocationSearch)(req, {
770
+ interpretPlusSignAsSpace: true
771
+ });
772
+ // we called URLSearchParam.toString for the output, any encoded/not encoded space will replace + with interpretsPlusSignAsSpace is true
773
+ expect(output).toBe('?q=mens+shirt+dresses');
774
+ });
775
+ test('not interpret + sign as space when interpretsPlusSignAsSpace is set to false in config', () => {
776
+ const req = {
777
+ originalUrl: '/hello-word?q=mens+shirt',
778
+ query: {
779
+ q: 'mens+shirt'
780
+ }
781
+ };
782
+ // we called URLSearchParam.toString for the output, with interpretsPlusSignAsSpace is false, it will encode literally + to %2B
783
+ const output = (0, _reactRendering.getLocationSearch)(req);
784
+ expect(output).toBe('?q=mens%2Bshirt');
785
+ });
786
+ });