inertia-sails 1.3.2 → 1.4.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 (41) hide show
  1. package/README.md +55 -2
  2. package/index.js +133 -81
  3. package/lib/handle-bad-request.js +20 -6
  4. package/lib/helpers/build-page-object.js +117 -12
  5. package/lib/helpers/inertia-headers.js +4 -1
  6. package/lib/helpers/is-inertia-partial-request.js +10 -0
  7. package/lib/helpers/is-inertia-request.js +9 -0
  8. package/lib/helpers/request-context.js +49 -8
  9. package/lib/helpers/resolve-asset-version.js +12 -0
  10. package/lib/helpers/resolve-validation-errors.js +12 -5
  11. package/lib/location.js +11 -0
  12. package/lib/middleware/inertia-middleware.js +7 -2
  13. package/lib/props/always-prop.js +6 -2
  14. package/lib/props/defer-prop.js +46 -6
  15. package/lib/props/get-partial-data.js +9 -0
  16. package/lib/props/merge-prop.js +7 -4
  17. package/lib/props/merge-targets.js +114 -0
  18. package/lib/props/mergeable-prop.js +87 -0
  19. package/lib/props/once-prop.js +9 -1
  20. package/lib/props/optional-prop.js +12 -4
  21. package/lib/props/pick-props-to-resolve.js +14 -1
  22. package/lib/props/resolve-deferred-props.js +17 -1
  23. package/lib/props/resolve-except-props.js +11 -0
  24. package/lib/props/resolve-merge-props.js +81 -20
  25. package/lib/props/resolve-once-props.js +17 -7
  26. package/lib/props/resolve-only-props.js +10 -3
  27. package/lib/props/resolve-page-props.js +72 -13
  28. package/lib/props/resolve-scroll-props.js +17 -3
  29. package/lib/props/scroll-prop.js +30 -8
  30. package/lib/render.js +68 -9
  31. package/lib/responses/server-error.js +23 -6
  32. package/lib/types.js +68 -0
  33. package/package.json +3 -2
  34. package/test.js +53 -12
  35. package/tests/helpers/build-page-object.test.js +183 -0
  36. package/tests/helpers/preserve-fragment.test.js +97 -0
  37. package/tests/props/merge-targets.test.js +74 -0
  38. package/tests/props/resolve-merge-props.test.js +151 -0
  39. package/tests/props/resolve-page-props.test.js +70 -0
  40. package/tests/props/resolve-scroll-props.test.js +51 -0
  41. package/tests/render.test.js +197 -0
@@ -1,12 +1,25 @@
1
1
  const ScrollProp = require('./scroll-prop')
2
2
 
3
+ /**
4
+ * @typedef {import('../types').InertiaProps} InertiaProps
5
+ *
6
+ * @typedef {Object} ScrollPropsMetadata
7
+ * @property {Record<string, {
8
+ * pageName: string,
9
+ * currentPage: number,
10
+ * previousPage: number|null,
11
+ * nextPage: number|null,
12
+ * reset: boolean
13
+ * }>} [scrollProps]
14
+ */
15
+
3
16
  /**
4
17
  * Resolve scroll props metadata for the page response.
5
18
  * Extracts ScrollProp instances and builds the scrollProps object
6
- * expected by Inertia.js v2's <InfiniteScroll> component.
19
+ * expected by Inertia's <InfiniteScroll> component.
7
20
  *
8
- * @param {Object} pageProps - The page props
9
- * @returns {Object} - Object with scrollProps if any exist
21
+ * @param {InertiaProps} pageProps - The page props
22
+ * @returns {ScrollPropsMetadata} - Object with scrollProps if any exist
10
23
  *
11
24
  * @example
12
25
  * // Input props with ScrollProp instance:
@@ -26,6 +39,7 @@ const ScrollProp = require('./scroll-prop')
26
39
  * }
27
40
  */
28
41
  module.exports = function resolveScrollProps(pageProps) {
42
+ /** @type {NonNullable<ScrollPropsMetadata['scrollProps']>} */
29
43
  const scrollProps = {}
30
44
 
31
45
  for (const [key, value] of Object.entries(pageProps || {})) {
@@ -1,5 +1,17 @@
1
1
  const MergeProp = require('./merge-prop')
2
2
 
3
+ /**
4
+ * @typedef {import('../types').PropCallback} PropCallback
5
+ *
6
+ * @typedef {Object} ScrollPropOptions
7
+ * @property {number} [page=0] - Current page index (0-based for Waterline)
8
+ * @property {number} [perPage=10] - Items per page
9
+ * @property {number} [total=0] - Total number of items
10
+ * @property {string} [pageName='page'] - Query parameter name for pagination
11
+ * @property {string} [wrapper='data'] - Key to wrap the data in
12
+ * @property {string|null} [matchOn] - Optional field used to match items when merging
13
+ */
14
+
3
15
  /**
4
16
  * ScrollProp - Configures paginated data for infinite scrolling.
5
17
  *
@@ -20,13 +32,8 @@ const MergeProp = require('./merge-prop')
20
32
  */
21
33
  class ScrollProp extends MergeProp {
22
34
  /**
23
- * @param {Function} callback - Callback returning the paginated data array
24
- * @param {Object} [options] - Pagination options
25
- * @param {number} [options.page=0] - Current page index (0-based for Waterline)
26
- * @param {number} [options.perPage=10] - Items per page
27
- * @param {number} [options.total=0] - Total number of items
28
- * @param {string} [options.pageName='page'] - Query parameter name for pagination
29
- * @param {string} [options.wrapper='data'] - Key to wrap the data in
35
+ * @param {PropCallback} callback - Callback returning the paginated data array
36
+ * @param {ScrollPropOptions} [options] - Pagination options
30
37
  */
31
38
  constructor(callback, options = {}) {
32
39
  const {
@@ -34,7 +41,8 @@ class ScrollProp extends MergeProp {
34
41
  perPage = 10,
35
42
  total = 0,
36
43
  pageName = 'page',
37
- wrapper = 'data'
44
+ wrapper = 'data',
45
+ matchOn = null
38
46
  } = options
39
47
 
40
48
  // Calculate pagination metadata
@@ -45,6 +53,7 @@ class ScrollProp extends MergeProp {
45
53
  const hasPreviousPage = currentPage > 1
46
54
 
47
55
  // Wrap the callback to return structured data with metadata
56
+ /** @type {() => Promise<any>} */
48
57
  const wrappedCallback = async () => {
49
58
  const data = typeof callback === 'function' ? await callback() : callback
50
59
 
@@ -66,13 +75,26 @@ class ScrollProp extends MergeProp {
66
75
 
67
76
  super(wrappedCallback)
68
77
 
78
+ // InfiniteScroll uses request headers to decide append vs prepend.
79
+ /** @type {import('../types').MergeOperation[]} */
80
+ this.mergeOperations = []
81
+
69
82
  // Store metadata for potential access
83
+ /** @type {number} */
70
84
  this.page = page
85
+ /** @type {number} */
71
86
  this.perPage = perPage
87
+ /** @type {number} */
72
88
  this.total = total
89
+ /** @type {string} */
73
90
  this.pageName = pageName
91
+ /** @type {string} */
74
92
  this.wrapper = wrapper
93
+ /** @type {string|null} */
94
+ this.matchOnPath = matchOn
95
+ /** @type {number} */
75
96
  this.totalPages = totalPages
97
+ /** @type {number} */
76
98
  this.currentPage = currentPage
77
99
  }
78
100
  }
package/lib/render.js CHANGED
@@ -2,7 +2,60 @@ const { encode } = require('querystring')
2
2
  const inertiaHeaders = require('./helpers/inertia-headers')
3
3
  const buildPageObject = require('./helpers/build-page-object')
4
4
  const requestContext = require('./helpers/request-context')
5
+ const resolveAssetVersion = require('./helpers/resolve-asset-version')
5
6
 
7
+ /**
8
+ * @typedef {import('./types').InertiaRequest} InertiaRequest
9
+ * @typedef {import('./types').InertiaResponse} InertiaResponse
10
+ * @typedef {import('./types').InertiaProps} InertiaProps
11
+ *
12
+ * @typedef {Object} RenderData
13
+ * @property {string} page
14
+ * @property {InertiaProps} [props]
15
+ * @property {InertiaProps} [locals]
16
+ */
17
+
18
+ /**
19
+ * @param {InertiaRequest} req
20
+ * @returns {string}
21
+ */
22
+ function getRequestUrl(req) {
23
+ let url = req.url || req.originalUrl || '/'
24
+ const queryParams = req.query || {}
25
+
26
+ if (req.method === 'GET' && Object.keys(queryParams).length) {
27
+ // Only append query params if the URL doesn't already contain them
28
+ // This prevents duplication when redirecting with query parameters
29
+ if (!url.includes('?')) {
30
+ url += `?${encode(queryParams)}`
31
+ }
32
+ }
33
+
34
+ return url
35
+ }
36
+
37
+ /**
38
+ * @param {InertiaRequest} req
39
+ * @param {any} currentVersion
40
+ * @returns {boolean}
41
+ */
42
+ function hasAssetVersionMismatch(req, currentVersion) {
43
+ const requestVersion = req.get(inertiaHeaders.VERSION)
44
+
45
+ if (req.method !== 'GET') return false
46
+ if (!req.get(inertiaHeaders.INERTIA)) return false
47
+ if (requestVersion === undefined || requestVersion === null) return false
48
+ if (currentVersion === undefined || currentVersion === null) return false
49
+
50
+ return String(requestVersion) !== String(currentVersion)
51
+ }
52
+
53
+ /**
54
+ * @param {InertiaRequest} req
55
+ * @param {InertiaResponse} res
56
+ * @param {RenderData} data
57
+ * @returns {Promise<any>}
58
+ */
6
59
  module.exports = async function render(req, res, data) {
7
60
  const sails = req._sails
8
61
  // Use request-scoped rootView if set, otherwise fall back to config
@@ -15,20 +68,26 @@ module.exports = async function render(req, res, data) {
15
68
  ...data.locals
16
69
  }
17
70
 
18
- let page = await buildPageObject(req, data.page, data.props)
71
+ const currentVersion = resolveAssetVersion(sails)
72
+ const requestUrl = getRequestUrl(req)
19
73
 
20
- const queryParams = req.query
21
- if (req.method === 'GET' && Object.keys(queryParams).length) {
22
- // Only append query params if the URL doesn't already contain them
23
- // This prevents duplication when redirecting with query parameters
24
- if (!page.url.includes('?')) {
25
- page.url += `?${encode(queryParams)}`
26
- }
74
+ if (hasAssetVersionMismatch(req, currentVersion)) {
75
+ res.set('Vary', 'X-Inertia')
76
+ res.set(inertiaHeaders.LOCATION, requestUrl)
77
+ return res.status(409).end()
27
78
  }
28
79
 
80
+ let page = await buildPageObject(
81
+ /** @type {any} */ (req),
82
+ data.page,
83
+ data.props
84
+ )
85
+
86
+ page.url = requestUrl
87
+
29
88
  if (req.get(inertiaHeaders.INERTIA)) {
30
89
  res.set(inertiaHeaders.INERTIA, true)
31
- res.set('Vary', 'Accept')
90
+ res.set('Vary', 'X-Inertia')
32
91
  return res.json(page)
33
92
  } else {
34
93
  // Implements full page reload
@@ -1,3 +1,10 @@
1
+ /**
2
+ * @typedef {import('../types').InertiaRequest} InertiaRequest
3
+ * @typedef {import('../types').InertiaResponse} InertiaResponse
4
+ * @typedef {import('../types').ErrorHtmlData} ErrorHtmlData
5
+ * @typedef {{ message?: string, stack?: string, name?: string }} ErrorLike
6
+ */
7
+
1
8
  /**
2
9
  * Server Error Response for Inertia.js
3
10
  *
@@ -9,9 +16,9 @@
9
16
  * in a modal overlay during development, allowing developers to see stack traces
10
17
  * without losing their page state.
11
18
  *
12
- * @param {Object} req - Express/Sails request object
13
- * @param {Object} res - Express/Sails response object
14
- * @param {Object|Error} [error] - Optional error data or Error object
19
+ * @param {InertiaRequest} req - Express/Sails request object
20
+ * @param {InertiaResponse} res - Express/Sails response object
21
+ * @param {ErrorLike} [error] - Optional error data or Error object
15
22
  * @returns {*} - Response
16
23
  *
17
24
  * @example
@@ -29,7 +36,7 @@
29
36
  module.exports = function handleServerError(req, res, error) {
30
37
  const sails = req._sails
31
38
  const statusCode = 500
32
- const isInertiaRequest = req.header('X-Inertia')
39
+ const isInertiaRequest = req.header?.('X-Inertia')
33
40
  const isDevelopment = process.env.NODE_ENV !== 'production'
34
41
 
35
42
  // Log the error
@@ -49,8 +56,8 @@ module.exports = function handleServerError(req, res, error) {
49
56
  errorName,
50
57
  errorMessage,
51
58
  errorStack,
52
- url: req.url,
53
- method: req.method
59
+ url: req.url || '/',
60
+ method: req.method || 'GET'
54
61
  })
55
62
 
56
63
  res.status(statusCode)
@@ -76,6 +83,10 @@ module.exports = function handleServerError(req, res, error) {
76
83
  {
77
84
  error: isDevelopment ? error : null
78
85
  },
86
+ /**
87
+ * @param {Error|null} err
88
+ * @param {string} html
89
+ */
79
90
  (err, html) => {
80
91
  if (err) {
81
92
  // If view doesn't exist, send a basic response
@@ -91,6 +102,8 @@ module.exports = function handleServerError(req, res, error) {
91
102
 
92
103
  /**
93
104
  * Build an HTML error page for the Inertia modal
105
+ * @param {ErrorHtmlData} data
106
+ * @returns {string}
94
107
  */
95
108
  function buildErrorHtml({
96
109
  statusCode,
@@ -270,6 +283,8 @@ function buildErrorHtml({
270
283
 
271
284
  /**
272
285
  * Format stack trace with syntax highlighting
286
+ * @param {string} stack
287
+ * @returns {string}
273
288
  */
274
289
  function formatStackTrace(stack) {
275
290
  return stack
@@ -294,6 +309,8 @@ function formatStackTrace(stack) {
294
309
 
295
310
  /**
296
311
  * Escape HTML special characters
312
+ * @param {any} str
313
+ * @returns {string}
297
314
  */
298
315
  function escapeHtml(str) {
299
316
  if (!str) return ''
package/lib/types.js ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Shared JSDoc typedefs for inertia-sails.
3
+ *
4
+ * These keep editor type-checking useful without turning the package into
5
+ * TypeScript. The shapes intentionally stay permissive because Sails request,
6
+ * response, and hook objects are extended by applications and other hooks.
7
+ *
8
+ * @typedef {Record<string, any>} InertiaProps
9
+ *
10
+ * @typedef {Object} SailsLike
11
+ * @property {Record<string, any>} config
12
+ * @property {{ info?: (...args: any[]) => void, warn?: (...args: any[]) => void, error?: (...args: any[]) => void }} [log]
13
+ * @property {Record<string, any>} [inertia]
14
+ * @property {{ bind?: (...args: any[]) => any }} [router]
15
+ * @property {(event: string, callback: (...args: any[]) => any) => any} [on]
16
+ *
17
+ * @typedef {Object} InertiaRequest
18
+ * @property {SailsLike} _sails
19
+ * @property {string} [method]
20
+ * @property {string} [url]
21
+ * @property {string} [originalUrl]
22
+ * @property {Record<string, any>} [query]
23
+ * @property {Record<string, any>} [headers]
24
+ * @property {Record<string, any>} [session]
25
+ * @property {boolean} [isSocket]
26
+ * @property {(name: string) => any} get
27
+ * @property {(name: string) => any} [header]
28
+ * @property {(key: string, value?: any) => any[]} [flash]
29
+ *
30
+ * @typedef {Object} InertiaResponse
31
+ * @property {(name: string, value: any) => InertiaResponse} [set]
32
+ * @property {(code: number) => InertiaResponse} [status]
33
+ * @property {(code: number) => any} [sendStatus]
34
+ * @property {(body?: any) => any} [send]
35
+ * @property {(body: any) => any} [json]
36
+ * @property {(...args: any[]) => any} [redirect]
37
+ * @property {(view: string, data?: any, callback?: (err: Error | null, html: string) => any) => any} [view]
38
+ * @property {(type: string) => InertiaResponse} [type]
39
+ * @property {() => any} [end]
40
+ *
41
+ * @typedef {() => any | Promise<any>} PropCallback
42
+ *
43
+ * @typedef {Object} MergeOperation
44
+ * @property {'append'|'prepend'|string} direction
45
+ * @property {string|null} path
46
+ * @property {string|null} matchOn
47
+ * @property {boolean} [isDefault]
48
+ *
49
+ * @typedef {Object} MergeOptions
50
+ * @property {string|Record<string, string|null>} [matchOn]
51
+ *
52
+ * @typedef {Object} ResolvedPageProps
53
+ * @property {InertiaProps} props
54
+ * @property {string[]} rescuedProps
55
+ *
56
+ * @typedef {Object} BadRequestData
57
+ * @property {Array<string|Record<string, string>>} [problems]
58
+ *
59
+ * @typedef {Object} ErrorHtmlData
60
+ * @property {number} statusCode
61
+ * @property {string} errorName
62
+ * @property {string} errorMessage
63
+ * @property {string} errorStack
64
+ * @property {string} url
65
+ * @property {string} method
66
+ */
67
+
68
+ module.exports = {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "inertia-sails",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "The Sails adapter for Inertia.",
5
5
  "main": "index.js",
6
6
  "sails": {
@@ -8,7 +8,8 @@
8
8
  "hookName": "inertia"
9
9
  },
10
10
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1"
11
+ "check:js": "tsc --allowJs --checkJs --noEmit --skipLibCheck --target ES2022 --module commonjs --noImplicitAny index.js test.js lib/**/*.js tests/**/*.js",
12
+ "test": "npm run check:js && node --test tests/*.test.js tests/**/*.test.js"
12
13
  },
13
14
  "repository": {
14
15
  "type": "git",
package/test.js CHANGED
@@ -24,11 +24,13 @@ const { INERTIA, VERSION } = require('./lib/helpers/inertia-headers')
24
24
  * @typedef {Object} InertiaPage
25
25
  * @property {string} component - The component name
26
26
  * @property {string} url - The current URL
27
- * @property {Object} props - The page props
28
- * @property {Object} [flash] - Flash data
27
+ * @property {Record<string, any>} props - The page props
28
+ * @property {Record<string, any>} [flash] - Flash data
29
+ * @property {boolean} [preserveFragment] - Whether URL fragments are preserved after redirects
29
30
  * @property {string[]} [mergeProps] - Props to merge
30
31
  * @property {string[]} [deepMergeProps] - Props to deep merge
31
- * @property {Object} [deferredProps] - Deferred props by group
32
+ * @property {Record<string, string[]>} [deferredProps] - Deferred props by group
33
+ * @property {string[]} [rescuedProps] - Deferred props rescued after callback failures
32
34
  */
33
35
 
34
36
  /**
@@ -37,6 +39,19 @@ const { INERTIA, VERSION } = require('./lib/helpers/inertia-headers')
37
39
  * @property {number} statusCode - HTTP status code
38
40
  */
39
41
 
42
+ /**
43
+ * @typedef {Object} SailsRequestOptions
44
+ * @property {string} url - Request URL or verb/path pair
45
+ * @property {Record<string, any>} [data] - Request data
46
+ * @property {Record<string, any>} [headers] - Request headers
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} SailsTestApp
51
+ * @property {{ inertia?: { version?: any } }} config
52
+ * @property {(options: SailsRequestOptions, callback: (err: Error|null, response: SailsResponse, body: InertiaPage) => void) => void} request
53
+ */
54
+
40
55
  /**
41
56
  * InertiaTestResponse - Fluent assertions for Inertia page responses
42
57
  */
@@ -115,7 +130,7 @@ class InertiaTestResponse {
115
130
 
116
131
  /**
117
132
  * Assert props match expected values
118
- * @param {Object} expected - Object with expected key-value pairs
133
+ * @param {Record<string, any>} expected - Object with expected key-value pairs
119
134
  * @returns {InertiaTestResponse} - For chaining
120
135
  */
121
136
  assertProps(expected) {
@@ -135,7 +150,7 @@ class InertiaTestResponse {
135
150
  /**
136
151
  * Assert a prop value using a callback
137
152
  * @param {string} key - Prop key
138
- * @param {Function} callback - Callback receiving the value, should throw if invalid
153
+ * @param {(value: any) => void} callback - Callback receiving the value, should throw if invalid
139
154
  * @returns {InertiaTestResponse} - For chaining
140
155
  */
141
156
  assertProp(key, callback) {
@@ -233,9 +248,35 @@ class InertiaTestResponse {
233
248
  return this
234
249
  }
235
250
 
251
+ /**
252
+ * Assert rescuedProps contains specific keys
253
+ * @param {string[]} keys - Expected rescued prop keys
254
+ * @returns {InertiaTestResponse} - For chaining
255
+ */
256
+ assertRescuedProps(keys) {
257
+ const rescuedProps = this.page.rescuedProps || []
258
+ for (const key of keys) {
259
+ if (!rescuedProps.includes(key)) {
260
+ throw new Error(`Expected "${key}" in rescuedProps`)
261
+ }
262
+ }
263
+ return this
264
+ }
265
+
266
+ /**
267
+ * Assert preserveFragment metadata is enabled
268
+ * @returns {InertiaTestResponse} - For chaining
269
+ */
270
+ assertPreserveFragment() {
271
+ if (this.page.preserveFragment !== true) {
272
+ throw new Error('Expected preserveFragment to be true')
273
+ }
274
+ return this
275
+ }
276
+
236
277
  /**
237
278
  * Get the raw page object for custom assertions
238
- * @returns {Object} - The Inertia page object
279
+ * @returns {InertiaPage} - The Inertia page object
239
280
  */
240
281
  getPage() {
241
282
  return this.page
@@ -243,7 +284,7 @@ class InertiaTestResponse {
243
284
 
244
285
  /**
245
286
  * Get the raw props for custom assertions
246
- * @returns {Object} - The props object
287
+ * @returns {Record<string, any>} - The props object
247
288
  */
248
289
  getProps() {
249
290
  return this.page.props
@@ -252,14 +293,14 @@ class InertiaTestResponse {
252
293
  /**
253
294
  * Helper to get nested values using dot notation
254
295
  * @private
255
- * @param {Object} obj - The object to search
296
+ * @param {Record<string, any>} obj - The object to search
256
297
  * @param {string} path - Dot-notation path
257
298
  * @returns {*} - The value at the path
258
299
  */
259
300
  _getNestedValue(obj, path) {
260
301
  return path.split('.').reduce(
261
302
  /**
262
- * @param {Object} current
303
+ * @param {any} current
263
304
  * @param {string} key
264
305
  */
265
306
  (current, key) => {
@@ -289,14 +330,14 @@ class InertiaTestResponse {
289
330
 
290
331
  /**
291
332
  * Create Inertia testing utilities for a Sails instance
292
- * @param {Object} sails - The Sails application instance
293
- * @returns {Object} - Testing utilities
333
+ * @param {SailsTestApp} sails - The Sails application instance
334
+ * @returns {Record<string, any>} - Testing utilities
294
335
  */
295
336
  module.exports = function createInertiaTestUtils(sails) {
296
337
  return {
297
338
  /**
298
339
  * Make an Inertia request and return a test response
299
- * @param {string|Object} urlOrOptions - URL string or request options
340
+ * @param {string|SailsRequestOptions} urlOrOptions - URL string or request options
300
341
  * @returns {Promise<InertiaTestResponse>} - Test response with assertions
301
342
  * @example
302
343
  * // Simple GET
@@ -0,0 +1,183 @@
1
+ const { describe, it } = require('node:test')
2
+ const assert = require('node:assert/strict')
3
+ const buildPageObject = require('../../lib/helpers/build-page-object')
4
+ const DeferProp = require('../../lib/props/defer-prop')
5
+ const {
6
+ INERTIA,
7
+ PARTIAL_COMPONENT,
8
+ PARTIAL_DATA
9
+ } = require('../../lib/helpers/inertia-headers')
10
+
11
+ /**
12
+ * @param {Record<string, any>} object
13
+ * @param {string} key
14
+ * @returns {boolean}
15
+ */
16
+ function hasOwn(object, key) {
17
+ return Object.prototype.hasOwnProperty.call(object, key)
18
+ }
19
+
20
+ function createRequest({
21
+ sharedProps = {},
22
+ pageProps = {},
23
+ clearHistory = false,
24
+ encryptHistory = false,
25
+ preserveFragment = false,
26
+ flash = {},
27
+ headers = {},
28
+ version = 'test-version',
29
+ url = '/dashboard'
30
+ } = {}) {
31
+ const normalizedHeaders = Object.fromEntries(
32
+ Object.entries(headers).map(([key, value]) => [key.toLowerCase(), value])
33
+ )
34
+
35
+ return {
36
+ url,
37
+ /**
38
+ * @param {string} header
39
+ * @returns {any}
40
+ */
41
+ get(header) {
42
+ return normalizedHeaders[header.toLowerCase()]
43
+ },
44
+ _sails: {
45
+ config: {
46
+ inertia: {
47
+ version
48
+ }
49
+ },
50
+ inertia: {
51
+ getShared() {
52
+ return sharedProps
53
+ },
54
+ shouldClearHistory() {
55
+ return clearHistory
56
+ },
57
+ shouldEncryptHistory() {
58
+ return encryptHistory
59
+ },
60
+ consumePreserveFragment() {
61
+ return preserveFragment
62
+ },
63
+ consumeFlash() {
64
+ return flash
65
+ }
66
+ }
67
+ },
68
+ pageProps
69
+ }
70
+ }
71
+
72
+ describe('buildPageObject', function () {
73
+ it('omits false history flags from the page object', async function () {
74
+ const req = createRequest()
75
+ const page = await buildPageObject(req, 'dashboard/index', {})
76
+
77
+ assert.equal(hasOwn(page, 'clearHistory'), false)
78
+ assert.equal(hasOwn(page, 'encryptHistory'), false)
79
+ })
80
+
81
+ it('includes true history flags in the page object', async function () {
82
+ const req = createRequest({
83
+ clearHistory: true,
84
+ encryptHistory: true
85
+ })
86
+ const page = await buildPageObject(req, 'dashboard/index', {})
87
+
88
+ assert.equal(page.clearHistory, true)
89
+ assert.equal(page.encryptHistory, true)
90
+ })
91
+
92
+ it('omits false preserveFragment from the page object', async function () {
93
+ const req = createRequest()
94
+ const page = await buildPageObject(req, 'dashboard/index', {})
95
+
96
+ assert.equal(hasOwn(page, 'preserveFragment'), false)
97
+ })
98
+
99
+ it('includes true preserveFragment in the page object', async function () {
100
+ const req = createRequest({
101
+ preserveFragment: true
102
+ })
103
+ const page = await buildPageObject(req, 'dashboard/index', {})
104
+
105
+ assert.equal(page.preserveFragment, true)
106
+ })
107
+
108
+ it('adds sharedProps metadata for shared prop keys', async function () {
109
+ const req = createRequest({
110
+ sharedProps: {
111
+ auth: { user: { id: 1 } },
112
+ app: { name: 'Boring Stack' }
113
+ }
114
+ })
115
+ const page = await buildPageObject(req, 'dashboard/index', {
116
+ stats: { users: 10 }
117
+ })
118
+
119
+ assert.deepEqual(page.sharedProps, ['auth', 'app'])
120
+ assert.deepEqual(page.props.auth, { user: { id: 1 } })
121
+ assert.deepEqual(page.props.stats, { users: 10 })
122
+ })
123
+
124
+ it('keeps sharedProps metadata when page props override a shared key', async function () {
125
+ const req = createRequest({
126
+ sharedProps: {
127
+ auth: { user: { id: 1 } }
128
+ }
129
+ })
130
+ const page = await buildPageObject(req, 'dashboard/index', {
131
+ auth: { user: { id: 2 } }
132
+ })
133
+
134
+ assert.deepEqual(page.sharedProps, ['auth'])
135
+ assert.deepEqual(page.props.auth, { user: { id: 2 } })
136
+ })
137
+
138
+ it('omits sharedProps when no shared props are present', async function () {
139
+ const req = createRequest()
140
+ const page = await buildPageObject(req, 'dashboard/index', {})
141
+
142
+ assert.equal(hasOwn(page, 'sharedProps'), false)
143
+ })
144
+
145
+ it('includes rescuedProps when a rescuable deferred prop fails', async function () {
146
+ const req = createRequest({
147
+ headers: {
148
+ [INERTIA]: 'true',
149
+ [PARTIAL_COMPONENT]: 'dashboard/index',
150
+ [PARTIAL_DATA]: 'analytics'
151
+ }
152
+ })
153
+ const page = await buildPageObject(req, 'dashboard/index', {
154
+ analytics: new DeferProp(async () => {
155
+ throw new Error('Analytics service unavailable')
156
+ }).rescue()
157
+ })
158
+
159
+ assert.deepEqual(page.props, {})
160
+ assert.deepEqual(page.rescuedProps, ['analytics'])
161
+ })
162
+
163
+ it('omits merge metadata for rescued deferred props', async function () {
164
+ const req = createRequest({
165
+ headers: {
166
+ [INERTIA]: 'true',
167
+ [PARTIAL_COMPONENT]: 'dashboard/index',
168
+ [PARTIAL_DATA]: 'analytics'
169
+ }
170
+ })
171
+ const analytics = new DeferProp(async () => {
172
+ throw new Error('Analytics service unavailable')
173
+ })
174
+ analytics.append('data', 'id')
175
+ analytics.rescue()
176
+
177
+ const page = await buildPageObject(req, 'dashboard/index', { analytics })
178
+
179
+ assert.deepEqual(page.rescuedProps, ['analytics'])
180
+ assert.equal(hasOwn(page, 'mergeProps'), false)
181
+ assert.equal(hasOwn(page, 'matchPropsOn'), false)
182
+ })
183
+ })