diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 1384c27a..9fc969ef 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -19,8 +19,17 @@ jobs: strategy: fail-fast: false matrix: - ruby: ['3.1', '3.2', '3.3'] - rails: ['6.1', '7.0', '7.1', '7.2'] + ruby: ['3.0', '3.1', '3.2', '3.3'] + rails: ['6.1', '7.0', '7.1', '7.2', '8.0'] + exclude: + - ruby: '3.0' + rails: '8.0' + - ruby: '3.1' + rails: '8.0' + - ruby: '3.0' + rails: '7.2' + - ruby: '3.1' + rails: '7.2' runs-on: ubuntu-latest name: Test against Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }} diff --git a/README.md b/README.md index 92fc3303..698d9702 100644 --- a/README.md +++ b/README.md @@ -188,15 +188,12 @@ end } ``` -### Lazy Props +### Optional Props -On the front end, Inertia supports the concept of "partial reloads" where only the props requested are returned by the server. Sometimes, you may want to use this flow to avoid processing a particularly slow prop on the intial load. In this case, you can use Lazy props. Lazy props aren't evaluated unless they're specifically requested by name in a partial reload. +On the frontend, Inertia supports the concept of "partial reloads" where only the props requested are returned by the server. Sometimes, you may want to use this flow to avoid processing a particularly slow prop on the initial load. In this case, you can use Optional props. Optional props aren't evaluated unless they're specifically requested by name in a partial reload. ```ruby - inertia_share some_data: InertiaRails.lazy(lambda { some_very_slow_method }) - - # Using a Ruby block syntax - inertia_share some_data: InertiaRails.lazy { some_very_slow_method } + inertia_share some_data: InertiaRails.optional { some_very_slow_method } ``` ### Routing @@ -269,6 +266,14 @@ end __Default__: `false` +#### `encrypt_history` + + When enabled, you instruct Inertia to encrypt your app's history, it uses + the browser's built-in [`crypto` api](https://developer.mozilla.org/en-US/docs/Web/API/Crypto) + to encrypt the current page's data before pushing it to the history state. + + __Default__: `false` + #### `ssr_enabled` _(experimental)_ Whether to use a JavaScript server to pre-render your JavaScript pages, diff --git a/lib/inertia_rails.rb b/lib/inertia_rails.rb index ef6b0885..9d0c0a58 100644 --- a/lib/inertia_rails.rb +++ b/lib/inertia_rails.rb @@ -16,6 +16,8 @@ props: options[:props], view_data: options[:view_data], deep_merge: options[:deep_merge], + encrypt_history: options[:encrypt_history], + clear_history: options[:clear_history], ).render end diff --git a/lib/inertia_rails/configuration.rb b/lib/inertia_rails/configuration.rb index 02f73f05..881511a4 100644 --- a/lib/inertia_rails/configuration.rb +++ b/lib/inertia_rails/configuration.rb @@ -16,6 +16,9 @@ class Configuration # controller configuration. layout: true, + # Whether to encrypt the history state in the client. + encrypt_history: false, + # SSR options. ssr_enabled: false, ssr_url: 'http://localhost:13714', diff --git a/lib/inertia_rails/controller.rb b/lib/inertia_rails/controller.rb index ce61c744..5925d49e 100644 --- a/lib/inertia_rails/controller.rb +++ b/lib/inertia_rails/controller.rb @@ -123,7 +123,7 @@ def default_render end def redirect_to(options = {}, response_options = {}) - capture_inertia_errors(response_options) + capture_inertia_session_options(response_options) super end @@ -155,10 +155,11 @@ def inertia_location(url) head :conflict end - def capture_inertia_errors(options) - if (inertia_errors = options.dig(:inertia, :errors)) - session[:inertia_errors] = inertia_errors.to_hash - end + def capture_inertia_session_options(options) + return unless (inertia = options[:inertia]) + + session[:inertia_errors] = inertia[:errors].to_hash if inertia[:errors] + session[:inertia_clear_history] = inertia[:clear_history] if inertia[:clear_history] end end end diff --git a/lib/inertia_rails/defer_prop.rb b/lib/inertia_rails/defer_prop.rb new file mode 100644 index 00000000..fe05941e --- /dev/null +++ b/lib/inertia_rails/defer_prop.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module InertiaRails + class DeferProp < IgnoreOnFirstLoadProp + DEFAULT_GROUP = "default" + + attr_reader :group + + def initialize(group: nil, merge: nil, &block) + @group = group || DEFAULT_GROUP + @merge = merge + @block = block + end + + def merge? + @merge + end + end +end diff --git a/lib/inertia_rails/ignore_on_first_load_prop.rb b/lib/inertia_rails/ignore_on_first_load_prop.rb new file mode 100644 index 00000000..82253cd6 --- /dev/null +++ b/lib/inertia_rails/ignore_on_first_load_prop.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module InertiaRails + class IgnoreOnFirstLoadProp < BaseProp + end +end diff --git a/lib/inertia_rails/inertia_rails.rb b/lib/inertia_rails/inertia_rails.rb index 4a389496..3f74720e 100644 --- a/lib/inertia_rails/inertia_rails.rb +++ b/lib/inertia_rails/inertia_rails.rb @@ -1,6 +1,10 @@ require 'inertia_rails/base_prop' +require 'inertia_rails/ignore_on_first_load_prop' require 'inertia_rails/always_prop' require 'inertia_rails/lazy_prop' +require 'inertia_rails/optional_prop' +require 'inertia_rails/defer_prop' +require 'inertia_rails/merge_prop' require 'inertia_rails/configuration' module InertiaRails @@ -19,8 +23,20 @@ def lazy(value = nil, &block) LazyProp.new(value, &block) end + def optional(&block) + OptionalProp.new(&block) + end + def always(&block) AlwaysProp.new(&block) end + + def merge(&block) + MergeProp.new(&block) + end + + def defer(group: nil, merge: nil, &block) + DeferProp.new(group: group, merge: merge, &block) + end end end diff --git a/lib/inertia_rails/lazy_prop.rb b/lib/inertia_rails/lazy_prop.rb index b3c01d99..8d7af0f4 100644 --- a/lib/inertia_rails/lazy_prop.rb +++ b/lib/inertia_rails/lazy_prop.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true module InertiaRails - class LazyProp < BaseProp + class LazyProp < IgnoreOnFirstLoadProp def initialize(value = nil, &block) raise ArgumentError, 'You must provide either a value or a block, not both' if value && block + InertiaRails.deprecator.warn( + "`lazy` is deprecated and will be removed in InertiaRails 4.0, use `optional` instead." + ) + @value = value @block = block end diff --git a/lib/inertia_rails/merge_prop.rb b/lib/inertia_rails/merge_prop.rb new file mode 100644 index 00000000..e16a94c5 --- /dev/null +++ b/lib/inertia_rails/merge_prop.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module InertiaRails + class MergeProp < BaseProp + def initialize(*) + super + @merge = true + end + + def merge? + @merge + end + end +end diff --git a/lib/inertia_rails/middleware.rb b/lib/inertia_rails/middleware.rb index 0d0d0179..1d4acced 100644 --- a/lib/inertia_rails/middleware.rb +++ b/lib/inertia_rails/middleware.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module InertiaRails class Middleware def initialize(app) @@ -20,7 +22,10 @@ def response request = ActionDispatch::Request.new(@env) # Inertia errors are added to the session via redirect_to - request.session.delete(:inertia_errors) unless keep_inertia_errors?(status) + unless keep_inertia_session_options?(status) + request.session.delete(:inertia_errors) + request.session.delete(:inertia_clear_history) + end status = 303 if inertia_non_post_redirect?(status) @@ -29,7 +34,7 @@ def response private - def keep_inertia_errors?(status) + def keep_inertia_session_options?(status) redirect_status?(status) || stale_inertia_request? end diff --git a/lib/inertia_rails/optional_prop.rb b/lib/inertia_rails/optional_prop.rb new file mode 100644 index 00000000..8b0c6e9f --- /dev/null +++ b/lib/inertia_rails/optional_prop.rb @@ -0,0 +1,4 @@ +module InertiaRails + class OptionalProp < IgnoreOnFirstLoadProp + end +end diff --git a/lib/inertia_rails/renderer.rb b/lib/inertia_rails/renderer.rb index 348347f5..befe8c32 100644 --- a/lib/inertia_rails/renderer.rb +++ b/lib/inertia_rails/renderer.rb @@ -2,7 +2,7 @@ require 'net/http' require 'json' -require_relative "inertia_rails" +require_relative 'inertia_rails' module InertiaRails class Renderer @@ -12,9 +12,11 @@ class Renderer :controller, :props, :view_data, + :encrypt_history, + :clear_history ) - def initialize(component, controller, request, response, render_method, props: nil, view_data: nil, deep_merge: nil) + def initialize(component, controller, request, response, render_method, props: nil, view_data: nil, deep_merge: nil, encrypt_history: nil, clear_history: nil) @controller = controller @configuration = controller.__send__(:inertia_configuration) @component = resolve_component(component) @@ -24,6 +26,8 @@ def initialize(component, controller, request, response, render_method, props: n @props = props || controller.__send__(:inertia_view_assigns) @view_data = view_data || {} @deep_merge = !deep_merge.nil? ? deep_merge : configuration.deep_merge_shared_data + @encrypt_history = !encrypt_history.nil? ? encrypt_history : configuration.encrypt_history + @clear_history = clear_history || controller.session[:inertia_clear_history] || false end def render @@ -78,7 +82,7 @@ def computed_props if rendering_partial_component? partial_keys.none? || key.in?(partial_keys) || prop.is_a?(AlwaysProp) else - !prop.is_a?(LazyProp) + !prop.is_a?(IgnoreOnFirstLoadProp) end end @@ -97,12 +101,22 @@ def computed_props end def page - { + default_page = { component: component, props: computed_props, url: @request.original_fullpath, version: configuration.version, + encryptHistory: encrypt_history, + clearHistory: clear_history, } + + deferred_props = deferred_props_keys + default_page[:deferredProps] = deferred_props if deferred_props.present? + + merge_props = merge_props_keys + default_page[:mergeProps] = merge_props if merge_props.present? + + default_page end def deep_transform_values(hash, &block) @@ -121,8 +135,28 @@ def drop_partial_except_keys(hash) end end + def deferred_props_keys + return if rendering_partial_component? + + @props.each_with_object({}) do |(key, prop), result| + (result[prop.group] ||= []) << key if prop.is_a?(DeferProp) + end + end + + def merge_props_keys + @props.each_with_object([]) do |(key, prop), result| + if prop.try(:merge?) && reset_keys.exclude?(key) + result << key + end + end + end + def partial_keys - (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact.map(&:to_sym) + @partial_keys ||= (@request.headers['X-Inertia-Partial-Data'] || '').split(',').compact.map(&:to_sym) + end + + def reset_keys + (@request.headers['X-Inertia-Reset'] || '').split(',').compact.map(&:to_sym) end def partial_except_keys diff --git a/spec/dummy/app/controllers/inertia_config_test_controller.rb b/spec/dummy/app/controllers/inertia_config_test_controller.rb index 94790818..6744926d 100644 --- a/spec/dummy/app/controllers/inertia_config_test_controller.rb +++ b/spec/dummy/app/controllers/inertia_config_test_controller.rb @@ -5,6 +5,7 @@ class InertiaConfigTestController < ApplicationController ssr_url: "http://localhost:7777", layout: "test", version: "1.0", + encrypt_history: false, ) # Test that modules included in the same class can also call it. diff --git a/spec/dummy/app/controllers/inertia_encrypt_history_controller.rb b/spec/dummy/app/controllers/inertia_encrypt_history_controller.rb new file mode 100644 index 00000000..21a1bebc --- /dev/null +++ b/spec/dummy/app/controllers/inertia_encrypt_history_controller.rb @@ -0,0 +1,25 @@ +class InertiaEncryptHistoryController < ApplicationController + inertia_config( + encrypt_history: -> { action_name != "default_config" } + ) + + def default_config + render inertia: 'TestComponent' + end + + def encrypt_history + render inertia: 'TestComponent' + end + + def override_config + render inertia: 'TestComponent', encrypt_history: false + end + + def clear_history + render inertia: 'TestComponent', clear_history: true + end + + def clear_history_after_redirect + redirect_to :empty_test, inertia: {clear_history: true} + end +end diff --git a/spec/dummy/app/controllers/inertia_render_test_controller.rb b/spec/dummy/app/controllers/inertia_render_test_controller.rb index 5ab23d49..d48d54a4 100644 --- a/spec/dummy/app/controllers/inertia_render_test_controller.rb +++ b/spec/dummy/app/controllers/inertia_render_test_controller.rb @@ -10,10 +10,10 @@ def props def except_props render inertia: 'TestComponent', props: { flat: 'flat param', - lazy: InertiaRails.lazy('lazy param'), - nested_lazy: InertiaRails.lazy do + optional: InertiaRails.optional { 'optional param' }, + nested_optional: InertiaRails.optional do { - first: 'first nested lazy param', + first: 'first nested optional param', } end, nested: { @@ -36,7 +36,7 @@ def component end def vary_header - response.headers["Vary"] = 'Accept-Language' + response.headers['Vary'] = 'Accept-Language' render inertia: 'TestComponent' end @@ -52,14 +52,42 @@ def lazy_props } end + def optional_props + render inertia: 'TestComponent', props: { + regular: 1, + optional: InertiaRails.optional { 1 }, + another_optional: InertiaRails.optional { 1 }, + } + end + def always_props render inertia: 'TestComponent', props: { always: InertiaRails.always { 'always prop' }, regular: 'regular prop', - lazy: InertiaRails.lazy do - 'lazy prop' + optional: InertiaRails.optional do + 'optional prop' + end, + another_optional: InertiaRails.optional { 'another optional prop' } + } + end + + def merge_props + render inertia: 'TestComponent', props: { + merge: InertiaRails.merge { 'merge prop' }, + regular: 'regular prop', + deferred_merge: InertiaRails.defer(merge: true) { 'deferred and merge prop'}, + deferred: InertiaRails.defer { 'deferred' }, + } + end + + def deferred_props + render inertia: 'TestComponent', props: { + name: 'Brian', + sport: InertiaRails.defer(group: 'other') { 'basketball' }, + level: InertiaRails.defer do + 'worse than he believes' end, - another_lazy: InertiaRails.lazy(->{ 'another lazy prop' }) + grit: InertiaRails.defer { 'intense' } } end end diff --git a/spec/dummy/config/routes.rb b/spec/dummy/config/routes.rb index 44da7017..855f6687 100644 --- a/spec/dummy/config/routes.rb +++ b/spec/dummy/config/routes.rb @@ -30,8 +30,11 @@ get 'error_500' => 'inertia_test#error_500' get 'content_type_test' => 'inertia_test#content_type_test' get 'lazy_props' => 'inertia_render_test#lazy_props' + get 'optional_props' => 'inertia_render_test#optional_props' get 'always_props' => 'inertia_render_test#always_props' get 'except_props' => 'inertia_render_test#except_props' + get 'merge_props' => 'inertia_render_test#merge_props' + get 'deferred_props' => 'inertia_render_test#deferred_props' get 'non_inertiafied' => 'inertia_test#non_inertiafied' get 'instance_props_test' => 'inertia_rails_mimic#instance_props_test' @@ -58,4 +61,10 @@ get 'conditional_share_show' => 'inertia_conditional_sharing#show' get 'conditional_share_edit' => 'inertia_conditional_sharing#edit' get 'conditional_share_show_with_a_problem' => 'inertia_conditional_sharing#show_with_a_problem' + + get 'encrypt_history_default_config' => 'inertia_encrypt_history#default_config' + get 'encrypt_history_encrypt_history' => 'inertia_encrypt_history#encrypt_history' + get 'encrypt_history_override_config' => 'inertia_encrypt_history#override_config' + get 'encrypt_history_clear_history' => 'inertia_encrypt_history#clear_history' + post 'encrypt_history_clear_history_after_redirect' => 'inertia_encrypt_history#clear_history_after_redirect' end diff --git a/spec/inertia/configuration_spec.rb b/spec/inertia/configuration_spec.rb index 42135356..6d0b251c 100644 --- a/spec/inertia/configuration_spec.rb +++ b/spec/inertia/configuration_spec.rb @@ -29,6 +29,7 @@ ssr_enabled: true, ssr_url: "http://localhost:7777", version: "2.0", + encrypt_history: false, ) end end diff --git a/spec/inertia/encrypt_history_spec.rb b/spec/inertia/encrypt_history_spec.rb new file mode 100644 index 00000000..6f8e3c89 --- /dev/null +++ b/spec/inertia/encrypt_history_spec.rb @@ -0,0 +1,54 @@ +RSpec.describe 'Inertia encrypt history', type: :request do + let(:headers) { {'X-Inertia' => true} } + + context 'with default config' do + it 'returns encryptHistory false' do + get encrypt_history_default_config_path, headers: headers + + expect(response.parsed_body['encryptHistory']).to eq(false) + expect(response.parsed_body['clearHistory']).to eq(false) + end + end + + context 'with encrypt history config' do + it 'returns encryptHistory true' do + get encrypt_history_encrypt_history_path, headers: headers + + expect(response.parsed_body['encryptHistory']).to eq(true) + expect(response.parsed_body['clearHistory']).to eq(false) + end + end + + context 'with override config' do + + it 'returns encryptHistory false' do + get encrypt_history_override_config_path, headers: headers + + expect(response.parsed_body['encryptHistory']).to eq(false) + expect(response.parsed_body['clearHistory']).to eq(false) + end + end + + context 'with clear history' do + it 'returns clearHistory true' do + get encrypt_history_clear_history_path, headers: headers + + expect(response.parsed_body['clearHistory']).to eq(true) + end + end + + context 'with clear history on redirect' do + it 'returns clearHistory true after the redirect' do + post encrypt_history_clear_history_after_redirect_path, headers: headers + + expect(response.headers['Location']).to eq(empty_test_url) + expect(session[:inertia_clear_history]).to eq(true) + + follow_redirect! + expect(response.body).to include('"clearHistory":true') + + get empty_test_path, headers: headers + expect(response.parsed_body['clearHistory']).to eq(false) + end + end +end diff --git a/spec/inertia/lazy_prop_spec.rb b/spec/inertia/lazy_prop_spec.rb index 755e6f7c..df7f3da0 100644 --- a/spec/inertia/lazy_prop_spec.rb +++ b/spec/inertia/lazy_prop_spec.rb @@ -1,6 +1,18 @@ RSpec.describe InertiaRails::LazyProp do it_behaves_like 'base prop' + let(:deprecator) do + double(warn: nil).tap do |deprecator| + allow(InertiaRails).to receive(:deprecator).and_return(deprecator) + end + end + + it "is deprecated" do + expect(deprecator).to receive(:warn).with("`lazy` is deprecated and will be removed in InertiaRails 4.0, use `optional` instead.") + + described_class.new('value') + end + describe '#call' do subject(:call) { prop.call(controller) } let(:prop) { described_class.new('value') } diff --git a/spec/inertia/rendering_spec.rb b/spec/inertia/rendering_spec.rb index 9c49e562..0e995cbe 100644 --- a/spec/inertia/rendering_spec.rb +++ b/spec/inertia/rendering_spec.rb @@ -117,7 +117,7 @@ let(:headers) do { 'X-Inertia' => true, - 'X-Inertia-Partial-Data' => 'nested,nested_lazy', + 'X-Inertia-Partial-Data' => 'nested,nested_optional', 'X-Inertia-Partial-Except' => 'nested', 'X-Inertia-Partial-Component' => 'TestComponent', } @@ -128,7 +128,7 @@ it 'returns listed props without excepted' do expect(response.parsed_body['props']).to eq( 'always' => 'always prop', - 'nested_lazy' => { 'first' => 'first nested lazy param' }, + 'nested_optional' => { 'first' => 'first nested optional param' }, ) end @@ -142,9 +142,9 @@ it 'returns all regular and partial props except excepted' do expect(response.parsed_body['props']).to eq( 'flat' => 'flat param', - 'lazy' => 'lazy param', + 'optional' => 'optional param', 'always' => 'always prop', - 'nested_lazy' => { 'first' => 'first nested lazy param' }, + 'nested_optional' => { 'first' => 'first nested optional param' }, ) end end @@ -152,7 +152,7 @@ context 'when except always prop' do let(:headers) {{ 'X-Inertia' => true, - 'X-Inertia-Partial-Data' => 'nested_lazy', + 'X-Inertia-Partial-Data' => 'nested_optional', 'X-Inertia-Partial-Except' => 'always_prop', 'X-Inertia-Partial-Component' => 'TestComponent', }} @@ -160,7 +160,7 @@ it 'returns always prop anyway' do expect(response.parsed_body['props']).to eq( 'always' => 'always prop', - 'nested_lazy' => { 'first' => 'first nested lazy param' }, + 'nested_optional' => { 'first' => 'first nested optional param' }, ) end end @@ -169,7 +169,7 @@ let(:headers) do { 'X-Inertia' => true, - 'X-Inertia-Partial-Data' => 'nested_lazy', + 'X-Inertia-Partial-Data' => 'nested_optional', 'X-Inertia-Partial-Except' => 'unknown', 'X-Inertia-Partial-Component' => 'TestComponent', } @@ -178,7 +178,7 @@ it 'returns props' do expect(response.parsed_body['props']).to eq( 'always' => 'always prop', - 'nested_lazy' => { 'first' => 'first nested lazy param' }, + 'nested_optional' => { 'first' => 'first nested optional param' }, ) end end @@ -187,8 +187,8 @@ let(:headers) do { 'X-Inertia' => true, - 'X-Inertia-Partial-Data' => 'nested,nested_lazy', - 'X-Inertia-Partial-Except' => 'nested.first,nested_lazy.first', + 'X-Inertia-Partial-Data' => 'nested,nested_optional', + 'X-Inertia-Partial-Except' => 'nested.first,nested_optional.first', 'X-Inertia-Partial-Component' => 'TestComponent', } end @@ -197,7 +197,7 @@ expect(response.parsed_body['props']).to eq( 'always' => 'always prop', 'nested' => { 'second' => 'second nested param' }, - 'nested_lazy' => { 'first' => 'first nested lazy param' }, + 'nested_optional' => { 'first' => 'first nested optional param' }, ) end end @@ -232,24 +232,131 @@ end end + context 'optional prop rendering' do + context 'on first load' do + let(:page) { + InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { regular: 1}).send(:page) + } + before { get optional_props_path } + + it { is_expected.to include inertia_div(page) } + end + + context 'with a partial reload' do + let(:page) { + InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { regular: 1, optional: 1}).send(:page) + } + let(:headers) {{ + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'optional', + 'X-Inertia-Partial-Component' => 'TestComponent', + }} + + before { get optional_props_path, headers: headers } + + it { is_expected.to eq page.to_json } + end + end + context 'always prop rendering' do let(:headers) { { 'X-Inertia' => true } } before { get always_props_path, headers: headers } - it "returns non-optional props on first load" do - expect(response.parsed_body["props"]).to eq({"always" => 'always prop', "regular" => 'regular prop' }) + it 'returns non-optional props on first load' do + expect(response.parsed_body['props']).to eq({'always' => 'always prop', 'regular' => 'regular prop' }) end context 'with a partial reload' do let(:headers) {{ 'X-Inertia' => true, - 'X-Inertia-Partial-Data' => 'lazy', + 'X-Inertia-Partial-Data' => 'optional', 'X-Inertia-Partial-Component' => 'TestComponent', }} - it "returns listed and always props" do - expect(response.parsed_body["props"]).to eq({"always" => 'always prop', "lazy" => 'lazy prop' }) + it 'returns listed and always props' do + expect(response.parsed_body['props']).to eq({'always' => 'always prop', 'optional' => 'optional prop' }) + end + end + end + + context 'merged prop rendering' do + let(:headers) { { 'X-Inertia' => true } } + + before { get merge_props_path, headers: headers } + + it 'returns non-optional props and meta on first load' do + expect(response.parsed_body['props']).to eq('merge' => 'merge prop', 'regular' => 'regular prop') + expect(response.parsed_body['mergeProps']).to match_array(%w[merge deferred_merge]) + expect(response.parsed_body['deferredProps']).to eq('default' => %w[deferred_merge deferred]) + end + + context 'with a partial reload' do + let(:headers) {{ + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'deferred_merge', + 'X-Inertia-Partial-Component' => 'TestComponent', + }} + + it 'returns listed and merge props' do + expect(response.parsed_body['props']).to eq({'deferred_merge' => 'deferred and merge prop'}) + expect(response.parsed_body['mergeProps']).to match_array(%w[merge deferred_merge]) + expect(response.parsed_body['deferredProps']).to be_nil + end + end + + context 'with a reset header' do + let(:headers) {{ + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'deferred_merge', + 'X-Inertia-Partial-Component' => 'TestComponent', + 'X-Inertia-Reset' => 'deferred_merge' + }} + + it 'returns listed and merge props' do + expect(response.parsed_body['props']).to eq({'deferred_merge' => 'deferred and merge prop'}) + expect(response.parsed_body['mergeProps']).to match_array(%w[merge]) + expect(response.parsed_body['deferredProps']).to be_nil + end + end + end + + context 'deferred prop rendering' do + context 'on first load' do + let(:headers) { { 'X-Inertia' => true } } + + before { get deferred_props_path, headers: headers } + + it 'does not include defer props inside props in first load' do + expect(response.parsed_body['props']).to eq({ 'name' => 'Brian' }) + end + + it 'returns deferredProps' do + expect(response.parsed_body['deferredProps']).to eq( + 'default' => ['level', 'grit'], + 'other' => ['sport'] + ) + end + end + + context 'with a partial reload' do + let(:page) { + InertiaRails::Renderer.new('TestComponent', controller, request, response, '', props: { sport: 'basketball', level: 'worse than he believes', grit: 'intense' }).send(:page) + } + let(:headers) { { + 'X-Inertia' => true, + 'X-Inertia-Partial-Data' => 'level,grit', # Simulate default group + 'X-Inertia-Partial-Component' => 'TestComponent', + } } + + before { get deferred_props_path, headers: headers } + + it { is_expected.to eq page.to_json } + it { is_expected.to include('intense') } + it { is_expected.to include('worse') } + it { is_expected.not_to include('basketball') } + it 'does not deferredProps key in json' do + expect(response.parsed_body['deferredProps']).to eq(nil) end end end diff --git a/spec/inertia/ssr_spec.rb b/spec/inertia/ssr_spec.rb index 7bd09a18..434eaea9 100644 --- a/spec/inertia/ssr_spec.rb +++ b/spec/inertia/ssr_spec.rb @@ -21,6 +21,8 @@ props: {name: 'Brandon', sport: 'hockey'}, url: props_path, version: '1.0', + encryptHistory: false, + clearHistory: false, }.to_json, 'Content-Type' => 'application/json' ) @@ -58,6 +60,8 @@ props: {name: 'Brandon', sport: 'hockey'}, url: props_path, version: '1.0', + encryptHistory: false, + clearHistory: false, }.to_json, 'Content-Type' => 'application/json' ) @@ -67,7 +71,7 @@ it 'renders inertia without ssr as a fallback' do get props_path - expect(response.body).to include '
' + expect(response.body).to include '' end end end