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