def modal_wrapper_attributes
{
- class: ["modal", "fade", modal_options[:wrapper_class]],
+ class: ["modal", "fade", modal_options[:wrapper_class]].compact,
tabindex: "-1",
role: "dialog",
data: {
@@ -19,9 +37,10 @@ def modal_wrapper_attributes
}.deep_merge(modal_options.except(:class, :wrapper_class, :controller))
end
+ # Returns the HTML attributes to apply to the inner modal dialog (.modal-dialog)
def modal_dialog_attributes
{
- class: ["modal-dialog", modal_options[:class]],
+ class: ["modal-dialog", modal_options[:class]].compact,
role: "document"
}
end
diff --git a/app/helpers/trestle/navigation_helper.rb b/app/helpers/trestle/navigation_helper.rb
index 7c1ffc0b..94c96e4d 100644
--- a/app/helpers/trestle/navigation_helper.rb
+++ b/app/helpers/trestle/navigation_helper.rb
@@ -1,4 +1,5 @@
module Trestle
+ # [Internal]
module NavigationHelper
def current_navigation_item?(item)
current_page?(item.path) || (item.admin && current_admin?(item.admin))
diff --git a/app/helpers/trestle/pagination_helper.rb b/app/helpers/trestle/pagination_helper.rb
index 4764f8ee..ee6bfc55 100644
--- a/app/helpers/trestle/pagination_helper.rb
+++ b/app/helpers/trestle/pagination_helper.rb
@@ -1,4 +1,5 @@
module Trestle
+ # [Internal]
module PaginationHelper
# Custom version of Kaminari's page_entries_info helper to use a
# Trestle-scoped I18n key and add a delimiter to the total count.
diff --git a/app/helpers/trestle/params_helper.rb b/app/helpers/trestle/params_helper.rb
index 35a4b293..c50386ce 100644
--- a/app/helpers/trestle/params_helper.rb
+++ b/app/helpers/trestle/params_helper.rb
@@ -1,5 +1,16 @@
module Trestle
module ParamsHelper
+ # Returns a subset of the params "hash" (an instance of ActionController::Parameters)
+ # limited to only those keys that should be considered persistent throughout
+ # reordering and pagination.
+ #
+ # This could be a scope or the current order, but this may be extended by Trestle
+ # plugins and the application itself in `Trestle.config.persistent_params` to include
+ # search queries, filters, etc.
+ #
+ # By default this list is set to: [:sort, :order, :scope]
+ #
+ # Returns an instance of ActionController::Parameters.
def persistent_params
flat, nested = Trestle.config.persistent_params.partition { |p| !p.is_a?(Hash) }
nested = nested.inject({}) { |result, param| result.merge(param) }
diff --git a/app/helpers/trestle/sort_helper.rb b/app/helpers/trestle/sort_helper.rb
index 940516ab..cce91d36 100644
--- a/app/helpers/trestle/sort_helper.rb
+++ b/app/helpers/trestle/sort_helper.rb
@@ -1,20 +1,47 @@
module Trestle
module SortHelper
- def sort_link(text, field, options={})
- sort_link = SortLink.new(field, persistent_params, options)
+ # Renders a sort link for a table column header.
+ #
+ # The `sort` and `order` params are used to determine whether the sort link is
+ # active, and which is the current sort direction (asc or desc). CSS classes are
+ # applied accordingly which are used to render the appropriate icons alongside the link.
+ #
+ # The current set of `persistent_params` is merged with the new `sort`/`order` params
+ # to build the target link URL.
+ #
+ # text - Text or HTML content to render as the link label
+ # field - The name of the current field, which should match the `sort` param
+ # when the collection is being sorted by this field
+ # options - Hash of options (default: {}):
+ # :default - (Boolean) Set to true if the field is considered to be active
+ # even when the `sort` param is blank (default: false)
+ # :default_order - (String/Symbol) Specify the default collection order when
+ # the `order` param is blank (default: "asc")
+ #
+ # Examples
+ #
+ # <%= sort_link "Title", :title, default: true %>
+ #
+ # <%= sort_link "Created", :created_at, default_order: "desc" %>
+ #
+ # Returns a HTML-safe String.
+ def sort_link(text, field, **options)
+ sort_link = SortLink.new(field, persistent_params, **options)
link_to text, sort_link.params, class: sort_link.classes
end
class SortLink
attr_reader :field
- def initialize(field, params, options)
- @field, @params, @options = field, params, options
+ def initialize(field, params, default: false, default_order: "asc")
+ @field, @params = field, params
+
+ @default = default
+ @default_order = default_order.to_s
end
def active?
- @params[:sort] == field.to_s ||
- (@options[:default] && !@params.key?(:sort))
+ @params[:sort] == field.to_s || (default? && !@params.key?(:sort))
end
def params
@@ -33,8 +60,12 @@ def current_order
@params[:order] || default_order
end
+ def default?
+ @default
+ end
+
def default_order
- @options.fetch(:default_order, "asc").to_s
+ @default_order
end
def classes
diff --git a/app/helpers/trestle/status_helper.rb b/app/helpers/trestle/status_helper.rb
index 7a961dc8..36d37d12 100644
--- a/app/helpers/trestle/status_helper.rb
+++ b/app/helpers/trestle/status_helper.rb
@@ -1,8 +1,24 @@
module Trestle
module StatusHelper
- def status_tag(label, status=:primary, options={})
- options[:class] ||= ["badge", "badge-#{status}"]
- content_tag(:span, label, options)
+ # Renders a status indicator as a Bootstrap badge.
+ # (https://getbootstrap.com/docs/5.3/components/badge/)
+ #
+ # label - Status badge text or HTML content
+ # status - Status class (as .badge-{status}) to apply to the badge (default: :primary)
+ # attributes - Additional HTML attributes to add to the
tag
+ #
+ # Examples
+ #
+ # <%= status_tag("Status Text") %>
+ #
+ # <%= status_tag(icon("fas fa-check"), :success) %>
+ #
+ # <%= status_tag(safe_join([icon("fas fa-warning"), "Error"], " "), :danger,
+ # data: { controller: "tooltip" }, title: "Full error message") %>
+ #
+ # Returns a HTML-safe String.
+ def status_tag(label, status=:primary, **attributes)
+ tag.span(label, **attributes.merge(class: ["badge", "badge-#{status}", attributes[:class]]))
end
end
end
diff --git a/app/helpers/trestle/tab_helper.rb b/app/helpers/trestle/tab_helper.rb
index 9994f3dc..756dd0c0 100644
--- a/app/helpers/trestle/tab_helper.rb
+++ b/app/helpers/trestle/tab_helper.rb
@@ -1,13 +1,34 @@
module Trestle
module TabHelper
- def tabs
- @_trestle_tabs ||= {}
- end
+ # Creates a tab pane using content via the given block, :partial option or partial
+ # template automatically inferred from the tab name.
+ #
+ # It also appends a Trestle::Tab object to the list of declared tabs that is
+ # accessible via the #tabs helper (e.g. for rendering the tab links).
+ #
+ # name - (Symbol) Internal name for the tab
+ # options - Hash of options (default: {}):
+ # :label - Optional tab label. If not provided, will be inferred by the
+ # admin-translated tab name (`admin.tabs.{name}` i18n scope)
+ # :badge - Optional badge to show next to the tab label (e.g. a counter)
+ # :partial - Optional partial template name to use when a block is not provided
+ #
+ # Examples
+ #
+ # <%= tab :details %>
+ # => Automatically renders the 'details' partial (e.g. "_details.html.erb") as the tab content
+ #
+ # <%= tab :metadata, partial: "meta" %>
+ # => Renders the 'meta' partial (e.g. "_meta.html.erb") as the tab content
+ #
+ # <%= tab :comments do %> ...
+ # => Renders the given block as the tab content
+ #
+ # Returns a HTML-safe String.
+ def tab(name, **options)
+ tabs[name] = tab = Tab.new(name, **options)
- def tab(name, options={})
- tabs[name] = tab = Tab.new(name, options)
-
- content_tag(:div, id: tab.id(("modal" if modal_request?)), class: ["tab-pane", ('active' if name == tabs.keys.first)], role: "tabpanel") do
+ tag.div(id: tab.id(("modal" if modal_request?)), class: ["tab-pane", ('active' if name == tabs.keys.first)], role: "tabpanel") do
if block_given?
yield
elsif options[:partial]
@@ -17,5 +38,19 @@ def tab(name, options={})
end
end
end
+
+ # Returns a hash (name => Trestle::Tab) of the currently declared tabs.
+ def tabs
+ @_trestle_tabs ||= {}
+ end
+
+ # Captures the given block (using `content_for`) as the sidebar content.
+ def sidebar(&block)
+ content_for(:sidebar, &block)
+ end
+
+ def render_sidebar_as_tab?
+ modal_request? && content_for?(:sidebar)
+ end
end
end
diff --git a/app/helpers/trestle/table_helper.rb b/app/helpers/trestle/table_helper.rb
index 9cff5ebb..65629cff 100644
--- a/app/helpers/trestle/table_helper.rb
+++ b/app/helpers/trestle/table_helper.rb
@@ -2,15 +2,17 @@ module Trestle
module TableHelper
# Renders an existing named table or builds and renders a custom table if a block is provided.
#
- # name - The (optional) name of the table to render (as a Symbol), or the actual Trestle::Table instance itself.
- # options - Hash of options that will be passed to the table builder (default: {}):
- # :collection - The collection that should be rendered within the table. It should be an
- # Array-like object, but will most likely be an ActiveRecord scope. It can
- # also be a callable object (i.e. a Proc) in which case the result of calling
- # the block will be used as the collection.
- # See Trestle::Table::Builder for additional options.
- # block - An optional block that is passed to Trestle::Table::Builder to define a custom table.
- # One of either the name or block must be provided, but not both.
+ # One of either the name or block must be provided, but not both.
+ #
+ # name - The (optional) name of the table to render (as a Symbol), or the
+ # actual Trestle::Table instance itself
+ # collection - The collection that should be rendered within the table.
+ # It should be an Array-like object, but will most likely be an
+ # ActiveRecord scope. It can also be a callable object (i.e. a Proc) in
+ # which case the result of calling the block will be used as the collection
+ # options - Hash of options that will be passed to the table builder (default: {}).
+ # See Trestle::Table::Builder for additional options
+ # block - An optional block that is passed to Trestle::Table::Builder to define a custom table
#
# Examples
#
@@ -23,26 +25,23 @@ module TableHelper
# <%= table :accounts %>
#
# Returns the HTML representation of the table as a HTML-safe String.
- def table(name=nil, options={}, &block)
+ def table(name=nil, collection: nil, **options, &block)
if block_given?
- if name.is_a?(Hash)
- options = name
- else
- collection = name
- end
-
- table = Table::Builder.build(options, &block)
+ table = Table::Builder.build(**options, &block)
else
if name.is_a?(Trestle::Table)
table = name
else
- table = admin.tables.fetch(name) { raise ArgumentError, "Unable to find table named #{name.inspect}" }
+ table = admin.tables.fetch(name) {
+ raise ArgumentError, "Unable to find table named #{name.inspect}"
+ }
end
- table = table.with_options(options.reverse_merge(sortable: false))
+ table = table.with_options(sortable: false, **options)
end
- collection ||= options[:collection] || table.options[:collection]
+ collection ||= name if block_given?
+ collection ||= table.options[:collection]
collection = collection.call if collection.respond_to?(:call)
render "trestle/table/table", table: table, collection: collection
@@ -50,8 +49,9 @@ def table(name=nil, options={}, &block)
# Renders the pagination controls for a collection.
#
- # collection - The paginated Kaminari collection to render controls for (required).
- # options - Hash of options that will be passed to the Kaminari #paginate method (default: {}):
+ # collection - The paginated Kaminari collection to render controls for (required)
+ # entry_name - Custom item name passed to the Kaminari #page_entries_info helper
+ # options - Hash of options that will be passed to the Kaminari #paginate method (default: {})
#
# Examples
#
diff --git a/app/helpers/trestle/timestamp_helper.rb b/app/helpers/trestle/timestamp_helper.rb
index 2107c6f3..f96be44b 100644
--- a/app/helpers/trestle/timestamp_helper.rb
+++ b/app/helpers/trestle/timestamp_helper.rb
@@ -1,13 +1,13 @@
module Trestle
module TimestampHelper
- # Renders a Time object as a formatted timestamp (using a tag)
+ # Renders a Time object as a formatted timestamp (using a tag).
#
- # time - The Time object to format.
- # options - Hash of options (default: {}):
- # :class - Additional HTML classes to add to the tag.
- # :precision - Time precision, either :minutes or :seconds (default: :minutes).
- # :date_format - I18n date format to use for the date (default: :trestle_date).
- # :time_format - I18n time format to use for the time (default: :trestle_time).
+ # time - The Time object to format
+ # precision - Time precision, either :minutes or :seconds (default: :minutes)
+ # date_format - I18n date format to use for the date (default: :trestle_date)
+ # time_format - I18n time format to use for the time (default: :trestle_time,
+ # or :trestle_time_with_seconds if precision is :seconds)
+ # attributes - Additional HTML attributes to add to the tag
#
# Examples
#
@@ -16,28 +16,24 @@ module TimestampHelper
# <%= timestamp(Time.current, class: "timestamp-inline", precision: :seconds) %>
#
# Returns the HTML representation of the given Time.
- def timestamp(time, options={})
+ def timestamp(time, precision: Trestle.config.timestamp_precision, date_format: :trestle_date, time_format: nil, **attributes)
return unless time
- classes = ["timestamp", options[:class]].compact
- precision = options.fetch(:precision) { Trestle.config.timestamp_precision }
- date_format = options.fetch(:date_format) { :trestle_date }
- time_format = options.fetch(:time_format) { precision == :seconds ? :trestle_time_with_seconds : :trestle_time }
+ time_format ||= (precision == :seconds) ? :trestle_time_with_seconds : :trestle_time
- time_tag(time, class: classes) do
+ time_tag(time, **attributes.merge(class: ["timestamp", attributes[:class]])) do
safe_join([
- l(time, format: date_format, default: proc { |date| "#{date.day.ordinalize} %b %Y" }),
- content_tag(:small, l(time, format: time_format, default: "%l:%M:%S %p"))
+ tag.span(l(time, format: date_format, default: proc { |date| "#{date.day.ordinalize} %b %Y" })),
+ tag.small(l(time, format: time_format, default: "%l:%M:%S %p"))
], "\n")
end
end
- # Renders a Date object as formatted datestamp (using a tag)
+ # Renders a Date object as formatted datestamp (using a tag).
#
- # date - The Date object to format.
- # options - Hash of options (default: {}):
- # :class - Additional HTML classes to add to the tag.
- # :format - I18n date format to use (default: :trestle_calendar).
+ # date - The Date object to format
+ # format - I18n date format to use (default: :trestle_calendar)
+ # attributes - Additional HTML attributes to add to the tag
#
# Examples
#
@@ -46,13 +42,10 @@ def timestamp(time, options={})
# <%= datestamp(article.created_at, format: :trestle_date, class: "custom-datestamp") %>
#
# Returns the HTML representation of the given Date.
- def datestamp(date, options={})
+ def datestamp(date, format: :trestle_calendar, **attributes)
return unless date
- classes = ["datestamp", options[:class]].compact
- format = options.fetch(:format) { :trestle_calendar}
-
- time_tag(date, class: classes) do
+ time_tag(date, **attributes.merge(class: ["datestamp", attributes[:class]])) do
l(date, format: format, default: "%-m/%-d/%Y")
end
end
diff --git a/app/helpers/trestle/title_helper.rb b/app/helpers/trestle/title_helper.rb
index 65fd9b93..6c930dc3 100644
--- a/app/helpers/trestle/title_helper.rb
+++ b/app/helpers/trestle/title_helper.rb
@@ -1,5 +1,7 @@
module Trestle
module TitleHelper
+ # Returns the page title (if set using content_for), falling back to
+ # the titleized action name as a default if not set.
def title
content_for(:title) || default_title
end
diff --git a/app/helpers/trestle/toolbars_helper.rb b/app/helpers/trestle/toolbars_helper.rb
index 1f1660e0..7be4b0dd 100644
--- a/app/helpers/trestle/toolbars_helper.rb
+++ b/app/helpers/trestle/toolbars_helper.rb
@@ -1,9 +1,10 @@
module Trestle
+ # [Internal]
module ToolbarsHelper
def render_toolbar(toolbar, *args)
result = toolbar.groups(self, *args).map do |items|
if items.many?
- content_tag(:div, class: "btn-group", role: "group") do
+ tag.div(class: "btn-group", role: "group") do
safe_join(items, "\n")
end
else
diff --git a/app/helpers/trestle/turbo/frame_helper.rb b/app/helpers/trestle/turbo/frame_helper.rb
index 436cf8f1..4f4252a6 100644
--- a/app/helpers/trestle/turbo/frame_helper.rb
+++ b/app/helpers/trestle/turbo/frame_helper.rb
@@ -1,7 +1,23 @@
module Trestle
module Turbo
module FrameHelper
- def index_turbo_frame(options={}, &block)
+ # Renders a container for an index view. An index turbo frame
+ # is by default reloadable (it will be refreshed by the `reload` turbo stream
+ # action), and has the Turbo visit behavior always set to "advance".
+ #
+ # attributes - Additional HTML attributes to add to the tag
+ #
+ # Examples
+ #
+ # <%= index_turbo_frame do %> ...
+ #
+ # <%= index_turbo_frame id: "articles-index",
+ # data: {
+ # reloadable_url_value: admin.path(:articles)
+ # } do %> ...
+ #
+ # Returns a HTML-safe String.
+ def index_turbo_frame(**attributes, &block)
defaults = {
id: "index",
data: {
@@ -10,10 +26,23 @@ def index_turbo_frame(options={}, &block)
}
}
- content_tag("turbo-frame", defaults.merge(options), &block)
+ tag.turbo_frame(**defaults.merge(attributes), &block)
end
- def resource_turbo_frame(instance, options={}, &block)
+ # Renders a container for an instance/resource view. A resource
+ # turbo frame sets its DOM id from the given instance and has a default target of
+ # "_top" (except for modal requests).
+ #
+ # attributes - Additional HTML attributes to add to the tag
+ #
+ # Examples
+ #
+ # <%= resource_turbo_frame(article) do %> ...
+ #
+ # <%= resource_turbo_frame(article, id: dom_id(article, "comment")) %> ...
+ #
+ # Returns a HTML-safe String.
+ def resource_turbo_frame(instance, **attributes, &block)
defaults = {
id: dom_id(instance),
target: ("_top" unless modal_request?),
@@ -22,7 +51,7 @@ def resource_turbo_frame(instance, options={}, &block)
}
}
- content_tag("turbo-frame", defaults.merge(options), &block)
+ tag.turbo_frame(**defaults.merge(attributes), &block)
end
end
end
diff --git a/app/helpers/trestle/url_helper.rb b/app/helpers/trestle/url_helper.rb
index 4a9fa257..34181ef5 100644
--- a/app/helpers/trestle/url_helper.rb
+++ b/app/helpers/trestle/url_helper.rb
@@ -2,6 +2,36 @@ module Trestle
module UrlHelper
MODAL_ACTIONS = [:new, :show, :edit]
+ # Generates a link to an admin, optionally for a specific instance on a resourceful admin.
+ #
+ # It has a few additional conveniences over using the standard `link_to` helper:
+ #
+ # 1) It can automatically infer the admin from the given instance.
+ # 2) It will automatically add data-controller="modal-trigger" when linking to a form
+ # action that is set to show as a modal.
+ # 3) It sets data-turbo-frame appropriately for modal and non-modal contexts to ensure
+ # the admin can correctly detect modal requests.
+ #
+ # content - HTML or text content to use as the link content
+ # (will be ignored if a block is provided)
+ # instance_or_url - model instance, or explicit String path
+ # options - Hash of options (default: {})
+ # :admin - Optional explicit admin (symbol or admin class)
+ # :action - Controller action to generate the link to
+ # :params - Additional params to use when generating the link URL
+ # (all other options are forwarded to the `link_to` helper)
+ # block - Optional block to capture to use as the link content
+ #
+ # Examples
+ #
+ # <%= admin_link_to article.name, article %>
+ #
+ # <%= admin_link_to admin: :dashboard, action: :index do %>
+ # <%= icon "fas fa-gauge" %> Dashboard
+ # <% end %>
+ #
+ # Returns a HTML-safe String.
+ # Raises ActionController::UrlGenerationError if the admin cannot be automatically inferred.
def admin_link_to(content, instance_or_url=nil, options={}, &block)
# Block given - ignore content parameter and capture content from block
if block_given?
@@ -60,8 +90,23 @@ def admin_link_to(content, instance_or_url=nil, options={}, &block)
end
end
- def admin_url_for(instance, options={})
- admin = Trestle.lookup(options.delete(:admin)) if options.key?(:admin)
+ # Returns the admin path for a given instance.
+ #
+ # An admin can either be explicitly specified (as a symbol or admin class),
+ # or it can be automatically inferred based the instance type using `admin_for`.
+ #
+ # instance - The model instance to generate a path for
+ # admin - Optional admin (symbol or admin class)
+ # options - Hash of options to pass to `instance_path` or `path` admin methods
+ #
+ # Examples
+ #
+ # <%= admin_url_for(article, action: :edit) %>
+ # <%= admin_url_for(article, admin: :special_articles) %>
+ #
+ # Returns a String, or nil if the admin cannot be automatically inferred.
+ def admin_url_for(instance, admin: nil, **options)
+ admin = Trestle.lookup(admin) if admin
admin ||= admin_for(instance)
return unless admin
@@ -75,6 +120,15 @@ def admin_url_for(instance, options={})
end
end
+ # Looks up the registered Trestle admin for a given model instance.
+ #
+ # The lookup is performed on the global `Trestle::Registry` instance,
+ # which tracks admin resource models unless the resource was created
+ # with `register_model: false`.
+ #
+ # instance - The model instance to look up in the registry
+ #
+ # Returns a Trestle::Admin subclass or nil if no matching admin found.
def admin_for(instance)
Trestle.lookup_model(instance.class)
end
diff --git a/lib/trestle/hook/helpers.rb b/lib/trestle/hook/helpers.rb
index 2a683449..cc4c3f89 100644
--- a/lib/trestle/hook/helpers.rb
+++ b/lib/trestle/hook/helpers.rb
@@ -1,6 +1,25 @@
module Trestle
class Hook
module Helpers
+ # Evaluates any defined hooks with the given name and returns the result.
+ #
+ # Each hook is evaluated and passed any provided arguments, and the result
+ # is concatenated together. If no hooks are defined, and a block is passed
+ # to this helper, then the block will be evaluated instead.
+ #
+ # name - Name of hook to evaluate
+ # args - Arguments to pass to hook blocks
+ # block - Optional block to evaluate as a fallback if no hooks are defined
+ #
+ # Examples
+ #
+ # <%= hook("index.toolbar.primary", toolbar) %>
+ #
+ # <%= hook("view.title") do %>
+ # Default Title
+ # <% end %>
+ #
+ # Returns a HTML-safe string.
def hook(name, *args, &block)
hooks = hooks(name)
@@ -13,6 +32,8 @@ def hook(name, *args, &block)
end
end
+ # Returns true or false depending on whether there are any defined hooks
+ # (either on the current admin or globally) with the given name.
def hook?(name)
hooks(name).any?
end
diff --git a/lib/trestle/tab.rb b/lib/trestle/tab.rb
index 0cf3fde9..faef92a1 100644
--- a/lib/trestle/tab.rb
+++ b/lib/trestle/tab.rb
@@ -6,7 +6,7 @@ class Tab
attr_reader :name, :options
- def initialize(name, options={})
+ def initialize(name, **options)
@name, @options = name, options
end
diff --git a/lib/trestle/table/column.rb b/lib/trestle/table/column.rb
index cb907922..79c0e633 100644
--- a/lib/trestle/table/column.rb
+++ b/lib/trestle/table/column.rb
@@ -61,7 +61,8 @@ def header
return if options.key?(:header) && options[:header].in?([nil, false])
if @table.sortable? && @column.sortable?
- @template.sort_link(header_text, @column.sort_field, @column.sort_options)
+ link_options = @column.sort_options.slice(:default, :default_order)
+ @template.sort_link(header_text, @column.sort_field, **link_options)
else
header_text
end
@@ -69,7 +70,7 @@ def header
def content(instance)
value = column_value(instance)
- content = @template.format_value(value, options)
+ content = @template.format_value(value, **options)
if value.respond_to?(:id) && options[:link] != false
# Column value was a model instance (e.g. from an association).
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5352e18f..2faa1330 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -51,4 +51,8 @@
Object.send(:remove_const, const)
end
end
+
+ config.around(:example, :tz) do |example|
+ Time.use_zone(example.metadata[:tz]) { example.run }
+ end
end
diff --git a/spec/trestle/helpers/avatar_helper_spec.rb b/spec/trestle/helpers/avatar_helper_spec.rb
index 2c259f03..f8803d0d 100644
--- a/spec/trestle/helpers/avatar_helper_spec.rb
+++ b/spec/trestle/helpers/avatar_helper_spec.rb
@@ -1,16 +1,8 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/avatar_helper'
-
-describe Trestle::AvatarHelper do
- include Trestle::AvatarHelper
-
- include ActionView::Helpers::TagHelper
- include ActionView::Helpers::TextHelper
- include ActionView::Context
-
+describe Trestle::AvatarHelper, type: :helper do
describe "#avatar" do
- let(:image) { content_tag(:img, src: "avatar.png") }
+ let(:image) { tag.img(src: "avatar.png") }
it "renders an avatar" do
result = avatar { image }
diff --git a/spec/trestle/helpers/card_helper_spec.rb b/spec/trestle/helpers/card_helper_spec.rb
new file mode 100644
index 00000000..62d3ff8e
--- /dev/null
+++ b/spec/trestle/helpers/card_helper_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Trestle::CardHelper, type: :helper do
+ describe "#card" do
+ it "returns a .card containing a .card-body" do
+ result = card { "Content" }
+
+ expect(result).to have_tag("div.card") do
+ with_tag "div.card-body", text: "Content"
+ end
+ end
+
+ it "applies any additional attributes to the .card
tag (merging classes)" do
+ result = card(id: "mycard", class: "text-bg-primary") { "Content" }
+
+ expect(result).to have_tag("div#mycard.card.text-bg-primary") do
+ with_tag "div.card-body", text: "Content"
+ end
+ end
+
+ it "prepends a header if supplied" do
+ result = card(header: "Header Text")
+
+ expect(result).to have_tag("div.card") do
+ with_tag "header.card-header", text: "Header Text"
+ end
+ end
+
+ it "appends a footer if supplied" do
+ result = card(footer: "Footer Text")
+
+ expect(result).to have_tag("div.card") do
+ with_tag "footer.card-footer", text: "Footer Text"
+ end
+ end
+ end
+end
diff --git a/spec/trestle/helpers/container_helper_spec.rb b/spec/trestle/helpers/container_helper_spec.rb
new file mode 100644
index 00000000..aaea996d
--- /dev/null
+++ b/spec/trestle/helpers/container_helper_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Trestle::ContainerHelper, type: :helper do
+ describe "#container" do
+ it "captures the block and renders within .main-content-container > .main-content divs" do
+ result = container { "Main content" }
+
+ expect(result).to have_tag("div.main-content-container") do
+ with_tag "div.main-content", text: "Main content"
+ end
+ end
+
+ it "applies any additional attributes to the .main-content-container
tag (merging classes)" do
+ result = container(id: "mycontainer", class: "mycontainer-class")
+ expect(result).to have_tag("div#mycontainer.main-content-container.mycontainer-class")
+ end
+
+ context "with a sidebar" do
+ it "yields a capture block to provide sidebar content" do
+ result = container do |c|
+ c.sidebar { "Sidebar" }
+ "Main content"
+ end
+
+ expect(result).to have_tag("div.main-content-container") do
+ with_tag "div.main-content", text: "Main content"
+ with_tag "aside.main-content-sidebar", text: "Sidebar"
+ end
+ end
+
+ it "applies any additional attributes on the .main-content-sidebar
tag (merging classes)" do
+ result = container do |c|
+ c.sidebar(id: "mysidebar", class: "mysidebar-class") { "Sidebar" }
+ "Main content"
+ end
+
+ expect(result).to have_tag("div.main-content-container") do
+ with_tag "aside#mysidebar.main-content-sidebar.mysidebar-class"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/trestle/helpers/form_helper_spec.rb b/spec/trestle/helpers/form_helper_spec.rb
new file mode 100644
index 00000000..b4a7630d
--- /dev/null
+++ b/spec/trestle/helpers/form_helper_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Trestle::FormHelper, type: :helper do
+ let(:admin) { double(parameter_name: :article) }
+ let(:instance) { double }
+
+ let(:block) { Proc.new {} }
+ let(:builder) { double }
+ let(:html_tag) { double }
+
+ describe "#trestle_form_for" do
+ it "calls form_for with default options" do
+ expect(self).to receive(:form_for).with(instance, builder: Trestle::Form::Builder, as: :article, data: { controller: "keyboard-submit form-loading form-error" }, &block)
+ trestle_form_for(instance, &block)
+ end
+
+ it "allows appending a custom Stimulus data-controller" do
+ expect(self).to receive(:form_for).with(instance, builder: Trestle::Form::Builder, as: :article, data: { controller: "keyboard-submit form-loading form-error custom-form" })
+ trestle_form_for(instance, data: { controller: "custom-form" })
+ end
+
+ it "sets ActionView::Base.field_error_proc within the block" do
+ allow(self).to receive(:form_for).and_yield(builder)
+
+ trestle_form_for(instance) do
+ expect(::ActionView::Base.field_error_proc.call(html_tag, instance)).to eq(html_tag)
+ end
+ end
+
+ it "allows access to the form builder within the block using the #form helper" do
+ allow(self).to receive(:form_for).and_yield(builder)
+
+ trestle_form_for(instance) do |f|
+ expect(form).to eq(f)
+ end
+ end
+ end
+end
diff --git a/spec/trestle/helpers/format_helper_spec.rb b/spec/trestle/helpers/format_helper_spec.rb
index 0dc9b40d..7f42a709 100644
--- a/spec/trestle/helpers/format_helper_spec.rb
+++ b/spec/trestle/helpers/format_helper_spec.rb
@@ -1,101 +1,78 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/format_helper'
-
-describe Trestle::FormatHelper do
- include Trestle::FormatHelper
-
- before(:each) do
- allow(self).to receive(:truncate) { |v| v }
- end
+describe Trestle::FormatHelper, type: :helper do
+ include Trestle::DisplayHelper
+ include Trestle::IconHelper
+ include Trestle::StatusHelper
+ include Trestle::TimestampHelper
describe "#format_value" do
- it "automatically formats timestamp values" do
- time = Time.now
- timestamp = double
-
- expect(self).to receive(:timestamp).with(time).and_return(timestamp)
- expect(format_value(time)).to eq(timestamp)
- end
-
- it "automatically formats date values" do
- date = Date.today
- datestamp = double
-
- expect(self).to receive(:datestamp).with(date).and_return(datestamp)
- expect(format_value(date)).to eq(datestamp)
- end
-
- it "returns 'none' text for nil values" do
- blank = double
-
- expect(self).to receive(:content_tag).with(:span, "None", class: "blank").and_return(blank)
- expect(format_value(nil)).to eq(blank)
- end
-
- it "returns custom blank text when :blank String option provided" do
- blank = double
-
- expect(self).to receive(:content_tag).with(:span, "Empty", class: "blank").and_return(blank)
- expect(format_value(nil, blank: "Empty")).to eq(blank)
+ describe "autoformatting" do
+ it "automatically formats timestamp values" do
+ time = Time.now
+ expect(format_value(time)).to eq(timestamp(time))
+ end
+
+ it "automatically formats date values" do
+ date = Date.today
+ expect(format_value(date)).to eq(datestamp(date))
+ end
+
+ it "returns 'none' text for nil values" do
+ result = format_value(nil)
+ expect(result).to have_tag("span.blank", text: "None")
+ end
+
+ it "returns custom blank text when :blank String option provided" do
+ result = format_value(nil, blank: "Empty")
+ expect(result).to have_tag("span.blank", text: "Empty")
+ end
+
+ it "calls custom blank block when :blank option is callable" do
+ result = format_value(nil, blank: -> { icon("fa fa-ban") })
+ expect(result).to have_tag("i.fa.fa-ban")
+ end
+
+ it "automatically formats true values" do
+ expect(format_value(true)).to eq(status_tag(icon("fa fa-check"), :success))
+ end
+
+ it "leaves false values empty" do
+ expect(format_value(false)).to be_nil
+ end
+
+ it "calls display for model-like values" do
+ model = double(id: "123", display_name: "Display 123")
+ expect(format_value(model)).to eq("Display 123")
+ end
+
+ it "automatically formats array values" do
+ result = format_value(["First", "Second"])
+
+ expect(result).to have_tag("ol") do
+ with_tag "li", text: "First"
+ with_tag "li", text: "Second"
+ end
+ end
end
- it "calls custom blank block when :blank option is callable" do
- blank = double
-
- expect(self).to receive(:icon).with("fa fa-ban").and_return(blank)
- expect(format_value(nil, blank: -> { icon("fa fa-ban") })).to eq(blank)
+ it "formats value as currency with format: :currency" do
+ expect(format_value(123.45, format: :currency)).to eq("$123.45")
end
- it "automatically formats true values" do
- status = double
- icon = double
-
- expect(self).to receive(:icon).with("fa fa-check").and_return(icon)
- expect(self).to receive(:status_tag).with(icon, :success).and_return(status)
- expect(format_value(true)).to eq(status)
- end
+ it "formats values as tags with format: :tags" do
+ result = format_value(["First", "Second"], format: :tags)
- it "leaves false values empty" do
- expect(format_value(false)).to be_nil
+ expect(result).to have_tag("div.tag-list") do
+ with_tag "span.tag.tag-primary", text: "First"
+ with_tag "span.tag.tag-primary", text: "Second"
+ end
end
- it "calls display for model-like values" do
- representation = double
- model = double(id: "123")
-
- expect(self).to receive(:display).with(model).and_return(representation)
- expect(format_value(model)).to eq(representation)
- end
-
- it "automatically formats array values" do
- list, items = double, double
- first_item, second_item = double, double
-
- expect(self).to receive(:content_tag).with(:li, "First").and_return(first_item)
- expect(self).to receive(:content_tag).with(:li, "Second").and_return(second_item)
- expect(self).to receive(:safe_join).with([first_item, second_item], "\n").and_return(items)
- expect(self).to receive(:content_tag).with(:ol, items).and_return(list)
- expect(format_value(["First", "Second"])).to eq(list)
- end
-
- it "formats value as currency" do
- currency = double
-
- expect(self).to receive(:number_to_currency).with(123.45).and_return(currency)
- expect(format_value(123.45, format: :currency)).to eq(currency)
- end
-
- it "formats values as tags" do
- tags = double
- list = double
- first_tag, second_tag = double, double
-
- expect(self).to receive(:content_tag).with(:span, "First", class: "tag tag-primary").and_return(first_tag)
- expect(self).to receive(:content_tag).with(:span, "Second", class: "tag tag-primary").and_return(second_tag)
- expect(self).to receive(:safe_join).with([first_tag, second_tag]).and_return(tags)
- expect(self).to receive(:content_tag).with(:div, tags, class: "tag-list").and_return(list)
- expect(format_value(["First", "Second"], format: :tags)).to eq(list)
+ it "raises ArgumentError with an invalid format" do
+ expect {
+ format_value(123, format: :invalid)
+ }.to raise_error(ArgumentError, "unknown format: invalid")
end
end
end
diff --git a/spec/trestle/helpers/gravatar_helper_spec.rb b/spec/trestle/helpers/gravatar_helper_spec.rb
new file mode 100644
index 00000000..0220be66
--- /dev/null
+++ b/spec/trestle/helpers/gravatar_helper_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Trestle::GravatarHelper, type: :helper do
+ let(:email) { "sam@trestle.io" }
+
+ describe "#gravatar_image_tag" do
+ it "returns an image tag using the Gravatar URL for the given email" do
+ expect(gravatar_image_tag(email)).to have_tag("img", with: { src: "https://www.gravatar.com/avatar/b83de56aa24349d584baf052299494f1.png?d=mp" })
+ end
+
+ it "passes options to the gravatar_image_url helper" do
+ expect(gravatar_image_tag(email, size: 100, d: "retro")).to have_tag("img", with: { src: "https://www.gravatar.com/avatar/b83de56aa24349d584baf052299494f1.png?d=retro&size=100" })
+ end
+
+ it "is aliased as #gravatar" do
+ expect(gravatar(email)).to eq(gravatar_image_tag(email))
+ end
+ end
+
+ describe "#gravatar_image_url" do
+ it "returns the Gravatar URL for the given email" do
+ expect(gravatar_image_url(email)).to eq("https://www.gravatar.com/avatar/b83de56aa24349d584baf052299494f1.png?d=mp")
+ end
+
+ it "appends options as query params" do
+ expect(gravatar_image_url(email, size: 100, d: "retro")).to eq("https://www.gravatar.com/avatar/b83de56aa24349d584baf052299494f1.png?d=retro&size=100")
+ end
+ end
+end
diff --git a/spec/trestle/helpers/grid_helper_spec.rb b/spec/trestle/helpers/grid_helper_spec.rb
index be48e5e5..f41171bb 100644
--- a/spec/trestle/helpers/grid_helper_spec.rb
+++ b/spec/trestle/helpers/grid_helper_spec.rb
@@ -1,13 +1,6 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/grid_helper'
-
-describe Trestle::GridHelper do
- include Trestle::GridHelper
-
- include ActionView::Helpers::TagHelper
- include ActionView::Context
-
+describe Trestle::GridHelper, type: :helper do
describe "#row" do
it "creates a div with class 'row'" do
expect(row { "content" }).to have_tag(".row", text: "content")
diff --git a/spec/trestle/helpers/hook_helper_spec.rb b/spec/trestle/helpers/hook_helper_spec.rb
index 5832e4b9..7d2b4e9d 100644
--- a/spec/trestle/helpers/hook_helper_spec.rb
+++ b/spec/trestle/helpers/hook_helper_spec.rb
@@ -1,14 +1,6 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/hook_helper'
-
-describe Trestle::HookHelper do
- include Trestle::HookHelper
-
- include ActionView::Helpers::OutputSafetyHelper
- include ActionView::Helpers::CaptureHelper
- include ActionView::Context
-
+describe Trestle::HookHelper, type: :helper do
describe "#hook" do
it "calls and concatenates each hook" do
Trestle.config.hook("test-hook") { "abc" }
diff --git a/spec/trestle/helpers/i18n_helper_spec.rb b/spec/trestle/helpers/i18n_helper_spec.rb
index 05904879..435f4dff 100644
--- a/spec/trestle/helpers/i18n_helper_spec.rb
+++ b/spec/trestle/helpers/i18n_helper_spec.rb
@@ -1,10 +1,6 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/i18n_helper'
-
-describe Trestle::I18nHelper do
- include Trestle::I18nHelper
-
+describe Trestle::I18nHelper, type: :helper do
describe "#i18n_fallbacks" do
it "delegates to I18n.fallbacks if available" do
allow(I18n).to receive(:fallbacks).and_return({ ca: ["ca", "es-ES", "es"] }.with_indifferent_access)
diff --git a/spec/trestle/helpers/icon_helper_spec.rb b/spec/trestle/helpers/icon_helper_spec.rb
new file mode 100644
index 00000000..6638e4b8
--- /dev/null
+++ b/spec/trestle/helpers/icon_helper_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Trestle::IconHelper, type: :helper do
+ describe "#icon" do
+ it "returns an tag with the given classes" do
+ result = icon("fas", "fa-star")
+ expect(result).to have_tag("i.fas.fa-star")
+ end
+
+ it "applies any additional attributes to the tag (merging classes)" do
+ result = icon("fas", "fa-star", class: "fa-fw", id: "icon")
+ expect(result).to have_tag("i#icon.fas.fa-star.fa-fw")
+ end
+ end
+end
diff --git a/spec/trestle/helpers/modal_helper_spec.rb b/spec/trestle/helpers/modal_helper_spec.rb
new file mode 100644
index 00000000..f3c8f56b
--- /dev/null
+++ b/spec/trestle/helpers/modal_helper_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Trestle::ModalHelper, type: :helper do
+ describe "#modal_options!" do
+ it "merges hash with existing modal options" do
+ modal_options!(class: "modal-class")
+ modal_options!(id: "modal-id", controller: "modal-controller")
+
+ expect(modal_options).to eq({
+ class: "modal-class",
+ id: "modal-id",
+ controller: "modal-controller"
+ })
+ end
+ end
+
+ describe "#modal_wrapper_attributes" do
+ it "returns core HTML attributes for the modal wrapper element" do
+ expect(modal_wrapper_attributes).to eq({
+ class: ["modal", "fade"],
+ tabindex: "-1",
+ role: "dialog",
+ data: { controller: "modal" }
+ })
+ end
+
+ it "merges the wrapper class and id from modal_options if provided" do
+ modal_options!(wrapper_class: "custom-modal", id: "custom-modal-id")
+
+ expect(modal_wrapper_attributes).to eq({
+ class: ["modal", "fade", "custom-modal"],
+ tabindex: "-1",
+ role: "dialog",
+ data: { controller: "modal" },
+ id: "custom-modal-id"
+ })
+ end
+
+ it "merges teh controller from modal_options if provided" do
+ modal_options!(controller: "custom-modal")
+
+ expect(modal_wrapper_attributes).to eq({
+ class: ["modal", "fade"],
+ tabindex: "-1",
+ role: "dialog",
+ data: { controller: "modal custom-modal" }
+ })
+ end
+ end
+
+ describe "#modal_dialog_attributes" do
+ it "returns core HTML attributes for the modal-dialog element" do
+ expect(modal_dialog_attributes).to eq({ class: ["modal-dialog"], role: "document" })
+ end
+
+ it "merges the class from modal_options if provided" do
+ modal_options!(class: "modal-lg")
+ expect(modal_dialog_attributes).to eq({ class: ["modal-dialog", "modal-lg"], role: "document" })
+ end
+ end
+end
diff --git a/spec/trestle/helpers/navigation_helper_spec.rb b/spec/trestle/helpers/navigation_helper_spec.rb
index 74e3247c..235cfb42 100644
--- a/spec/trestle/helpers/navigation_helper_spec.rb
+++ b/spec/trestle/helpers/navigation_helper_spec.rb
@@ -1,10 +1,6 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/navigation_helper'
-
-describe Trestle::NavigationHelper do
- include Trestle::NavigationHelper
-
+describe Trestle::NavigationHelper, type: :helper do
describe "#current_admin?" do
context "when admin is undefined" do
it "returns false" do
diff --git a/spec/trestle/helpers/params_helper_spec.rb b/spec/trestle/helpers/params_helper_spec.rb
index b7eb5311..58cea4c7 100644
--- a/spec/trestle/helpers/params_helper_spec.rb
+++ b/spec/trestle/helpers/params_helper_spec.rb
@@ -1,10 +1,6 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/params_helper'
-
-describe Trestle::ParamsHelper do
- include Trestle::ParamsHelper
-
+describe Trestle::ParamsHelper, type: :helper do
let(:params) { ActionController::Parameters.new(sort: :field, order: "asc", ignore: "me") }
describe "#persistent_params" do
@@ -32,25 +28,23 @@
end
end
- if Rails.gem_version > Gem::Version.new("5.1")
- context "when Trestle.config.persistent_params contains a hash declaration" do
- before(:each) do
- expect(Trestle.config).to receive(:persistent_params).and_return([:sort, { hash: {} }, :order])
- end
-
- let(:params) do
- ActionController::Parameters.new({
- sort: :field,
- order: "asc",
- ignore: "me",
- hash: { key: "value" },
- ignore_hash: { abc: "123" }
- })
- end
-
- it "includes persistent hash param" do
- expect(persistent_params).to eq(ActionController::Parameters.new(sort: :field, order: "asc", hash: { key: "value" }).permit!)
- end
+ context "when Trestle.config.persistent_params contains a hash declaration" do
+ before(:each) do
+ expect(Trestle.config).to receive(:persistent_params).and_return([:sort, { hash: {} }, :order])
+ end
+
+ let(:params) do
+ ActionController::Parameters.new({
+ sort: :field,
+ order: "asc",
+ ignore: "me",
+ hash: { key: "value" },
+ ignore_hash: { abc: "123" }
+ })
+ end
+
+ it "includes persistent hash param" do
+ expect(persistent_params).to eq(ActionController::Parameters.new(sort: :field, order: "asc", hash: { key: "value" }).permit!)
end
end
end
diff --git a/spec/trestle/helpers/sort_helper_spec.rb b/spec/trestle/helpers/sort_helper_spec.rb
index d32f7263..936d9894 100644
--- a/spec/trestle/helpers/sort_helper_spec.rb
+++ b/spec/trestle/helpers/sort_helper_spec.rb
@@ -1,91 +1,91 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/sort_helper'
+describe Trestle::SortHelper, type: :helper do
+ describe Trestle::SortHelper::SortLink do
+ let(:options) { {} }
+ let(:params) { {} }
+ let(:parameters) { ActionController::Parameters.new(params) }
-describe Trestle::SortHelper::SortLink do
- let(:options) { {} }
- let(:params) { {} }
- let(:parameters) { ActionController::Parameters.new(params) }
-
- subject(:link) { Trestle::SortHelper::SortLink.new(:field, parameters, options)}
-
- describe "#active?" do
- context "when the sort param matches the field name" do
- let(:params) { { sort: "field" } }
- it { is_expected.to be_active }
- end
-
- context "when the sort param does not match the field name" do
- it { is_expected.to_not be_active }
- end
-
- context "on a default field" do
- let(:options) { { default: true } }
-
- context "when no sort param is present" do
- it { is_expected.to be_active }
- end
+ subject(:link) { Trestle::SortHelper::SortLink.new(:field, parameters, **options)}
+ describe "#active?" do
context "when the sort param matches the field name" do
let(:params) { { sort: "field" } }
it { is_expected.to be_active }
end
context "when the sort param does not match the field name" do
- let(:params) { { sort: "other" } }
it { is_expected.to_not be_active }
end
- end
- end
- describe "#params" do
- context "when inactive" do
- it "returns params for ascending order" do
- expect(link.params).to eq(ActionController::Parameters.new(sort: :field, order: "asc"))
+ context "on a default field" do
+ let(:options) { { default: true } }
+
+ context "when no sort param is present" do
+ it { is_expected.to be_active }
+ end
+
+ context "when the sort param matches the field name" do
+ let(:params) { { sort: "field" } }
+ it { is_expected.to be_active }
+ end
+
+ context "when the sort param does not match the field name" do
+ let(:params) { { sort: "other" } }
+ it { is_expected.to_not be_active }
+ end
end
end
- context "when active" do
- let(:params) { { sort: "field", order: "asc" } }
+ describe "#params" do
+ context "when inactive" do
+ it "returns params for ascending order" do
+ expect(link.params).to eq(ActionController::Parameters.new(sort: :field, order: "asc"))
+ end
+ end
+
+ context "when active" do
+ let(:params) { { sort: "field", order: "asc" } }
- it "returns params for opposite direction" do
- expect(link.params).to eq(ActionController::Parameters.new(sort: :field, order: "desc"))
+ it "returns params for opposite direction" do
+ expect(link.params).to eq(ActionController::Parameters.new(sort: :field, order: "desc"))
+ end
end
- end
- context "with existing params" do
- let(:params) { { q: "search query" } }
+ context "with existing params" do
+ let(:params) { { q: "search query" } }
- it "merges in sort params" do
- expect(link.params).to eq(ActionController::Parameters.new(q: "search query", sort: :field, order: "asc"))
+ it "merges in sort params" do
+ expect(link.params).to eq(ActionController::Parameters.new(q: "search query", sort: :field, order: "asc"))
+ end
end
end
- end
- describe "#classes" do
- context "when inactive" do
- it "consists of only the sort class" do
- expect(link.classes).to eq(["sort"])
+ describe "#classes" do
+ context "when inactive" do
+ it "consists of only the sort class" do
+ expect(link.classes).to eq(["sort"])
+ end
end
- end
- context "when active" do
- let(:params) { { sort: "field" } }
+ context "when active" do
+ let(:params) { { sort: "field" } }
- it "includes the active class" do
- expect(link.classes).to include("active")
- end
+ it "includes the active class" do
+ expect(link.classes).to include("active")
+ end
- it "includes the sort-asc class" do
- expect(link.classes).to include("sort-asc")
+ it "includes the sort-asc class" do
+ expect(link.classes).to include("sort-asc")
+ end
end
- end
- context "when active in descending order" do
- let(:params) { { sort: "field", order: "desc" } }
+ context "when active in descending order" do
+ let(:params) { { sort: "field", order: "desc" } }
- it "includes the sort-desc class" do
- expect(link.classes).to include("sort-desc")
+ it "includes the sort-desc class" do
+ expect(link.classes).to include("sort-desc")
+ end
end
end
end
diff --git a/spec/trestle/helpers/status_helper_spec.rb b/spec/trestle/helpers/status_helper_spec.rb
new file mode 100644
index 00000000..81932e14
--- /dev/null
+++ b/spec/trestle/helpers/status_helper_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Trestle::StatusHelper, type: :helper do
+ describe "#status_tag" do
+ it "returns a badge with the given label" do
+ result = status_tag("Status")
+ expect(result).to have_tag("span.badge.badge-primary", text: "Status")
+ end
+
+ it "sets the badge class based on the status if provided" do
+ result = status_tag("Warning", :warning)
+ expect(result).to have_tag("span.badge.badge-warning", text: "Warning")
+ end
+
+ it "applies any additional attributes to the badge (merging classes)" do
+ result = status_tag("Status", :info, class: "badge-pill", id: "status")
+ expect(result).to have_tag("span#status.badge.badge-info.badge-pill", text: "Status")
+ end
+
+ it "applies additional attributes if an explicit status is not provided" do
+ result = status_tag("Status", class: "badge-pill", id: "status")
+ expect(result).to have_tag("span#status.badge.badge-primary.badge-pill", text: "Status")
+ end
+ end
+end
diff --git a/spec/trestle/helpers/table_helper_spec.rb b/spec/trestle/helpers/table_helper_spec.rb
index a1a284d9..faf36dae 100644
--- a/spec/trestle/helpers/table_helper_spec.rb
+++ b/spec/trestle/helpers/table_helper_spec.rb
@@ -1,50 +1,53 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/table_helper'
-
-describe Trestle::TableHelper do
- include Trestle::TableHelper
-
+describe Trestle::TableHelper, type: :helper do
let(:admin) { double }
let(:collection) { double }
- let(:table_object) { double }
+ let(:table_object) { Trestle::Table.new }
- context "with a block" do
- let(:block) { Proc.new {} }
+ describe "#table" do
+ context "with a block" do
+ let(:block) { Proc.new {} }
- it "builds and renders a custom table" do
- expect(Trestle::Table::Builder).to receive(:build).with({ admin: admin, collection: collection }, &block).and_return(table_object)
- expect(self).to receive(:render).with("trestle/table/table", table: table_object, collection: collection)
+ it "builds and renders a custom table" do
+ expect(Trestle::Table::Builder).to receive(:build).with({ admin: admin }, &block).and_return(table_object)
+ expect(self).to receive(:render).with("trestle/table/table", table: table_object, collection: collection)
- table(admin: admin, collection: collection, &block)
- end
- end
+ table(admin: admin, collection: collection, &block)
+ end
- context "with a table name" do
- it "retrieves and renders the named table" do
- expect(admin).to receive(:tables).and_return({ index: table_object })
- expect(table_object).to receive(:with_options).with({ sortable: false, collection: collection }).and_return(table_object)
- expect(self).to receive(:render).with("trestle/table/table", table: table_object, collection: collection)
+ it "builds and renders using a positional collection argument" do
+ expect(Trestle::Table::Builder).to receive(:build).with({ admin: admin }, &block).and_return(table_object)
+ expect(self).to receive(:render).with("trestle/table/table", table: table_object, collection: collection)
- table(:index, collection: collection)
+ table(collection, admin: admin, &block)
+ end
end
- it "raises an ArgumentError if the named table cannot be found" do
- expect(admin).to receive(:tables).and_return({})
+ context "with a table name" do
+ it "retrieves and renders the named table" do
+ expect(admin).to receive(:tables).and_return({ index: table_object })
+ expect(table_object).to receive(:with_options).with({ sortable: false }).and_return(table_object)
+ expect(self).to receive(:render).with("trestle/table/table", table: table_object, collection: collection)
- expect {
- table(:missing, collection: collection)
- }.to raise_error(ArgumentError, "Unable to find table named :missing")
- end
- end
+ table(:index, collection: collection)
+ end
- context "with a Trestle::Table instance" do
- let(:table_object) { Trestle::Table.new }
+ it "raises an ArgumentError if the named table cannot be found" do
+ expect(admin).to receive(:tables).and_return({})
+
+ expect {
+ table(:missing, collection: collection)
+ }.to raise_error(ArgumentError, "Unable to find table named :missing")
+ end
+ end
- it "renders the given table" do
- expect(table_object).to receive(:with_options).with({ sortable: false, collection: collection }).and_return(table_object)
- expect(self).to receive(:render).with("trestle/table/table", table: table_object, collection: collection)
- table(table_object, collection: collection)
+ context "with a Trestle::Table instance" do
+ it "renders the given table" do
+ expect(table_object).to receive(:with_options).with({ sortable: false }).and_return(table_object)
+ expect(self).to receive(:render).with("trestle/table/table", table: table_object, collection: collection)
+ table(table_object, collection: collection)
+ end
end
end
end
diff --git a/spec/trestle/helpers/timestamp_helper_spec.rb b/spec/trestle/helpers/timestamp_helper_spec.rb
new file mode 100644
index 00000000..9500b646
--- /dev/null
+++ b/spec/trestle/helpers/timestamp_helper_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Trestle::TimestampHelper, type: :helper do
+ describe "#timestamp" do
+ let(:time) { Time.zone.local(2024, 8, 21, 12, 34, 56) }
+
+ it "returns nil if time given is nil" do
+ expect(timestamp(nil)).to be_nil
+ end
+
+ it "returns a tag with the given time", tz: "Australia/Perth" do
+ result = timestamp(time)
+
+ expect(result).to have_tag("time.timestamp", with: { datetime: "2024-08-21T12:34:56+08:00" }) do
+ with_tag "span", text: "21st Aug 2024"
+ with_tag "small", text: "12:34 PM"
+ end
+ end
+
+ it "applies any additional attributes to the tag (merging classes)" do
+ result = timestamp(time, id: "special-time", class: "timestamp-inline")
+ expect(result).to have_tag("time#special-time.timestamp.timestamp-inline")
+ end
+
+ it "renders the time with seconds if precision: :seconds is passed" do
+ result = timestamp(time, precision: :seconds)
+
+ expect(result).to have_tag("time.timestamp") do
+ with_tag "small", text: "12:34:56 PM"
+ end
+ end
+
+ it "accepts custom date and time formats" do
+ result = timestamp(time, date_format: "%B %d, %Y", time_format: "%H:%M")
+
+ expect(result).to have_tag("time.timestamp") do
+ with_tag "span", text: "August 21, 2024"
+ with_tag "small", text: "12:34"
+ end
+ end
+ end
+
+ describe "#datestamp" do
+ let(:date) { Date.new(2024, 8, 21) }
+
+ it "returns nil if date given is nil" do
+ expect(datestamp(nil)).to be_nil
+ end
+
+ it "returns a tag with the given date" do
+ result = datestamp(date)
+ expect(result).to have_tag("time.datestamp", text: "8/21/2024", with: { datetime: "2024-08-21" })
+ end
+
+ it "applies any additional attributes to the tag (merging classes)" do
+ result = datestamp(date, id: "special-date", class: "custom-datestamp")
+ expect(result).to have_tag("time#special-date.datestamp.custom-datestamp")
+ end
+
+ it "accepts a custom l10n format" do
+ result = datestamp(date, format: :long)
+ expect(result).to have_tag("time.datestamp", text: "August 21, 2024")
+ end
+ end
+end
diff --git a/spec/trestle/helpers/title_helper_spec.rb b/spec/trestle/helpers/title_helper_spec.rb
index 48ba5575..56be764f 100644
--- a/spec/trestle/helpers/title_helper_spec.rb
+++ b/spec/trestle/helpers/title_helper_spec.rb
@@ -1,15 +1,6 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/title_helper'
-
-describe Trestle::TitleHelper do
- include Trestle::TitleHelper
-
- include ActionView::Helpers::CaptureHelper
- include ActionView::Context
-
- before(:each) { _prepare_context }
-
+describe Trestle::TitleHelper, type: :helper do
let(:action_name) { "default" }
describe "#title" do
diff --git a/spec/trestle/helpers/url_helper_spec.rb b/spec/trestle/helpers/url_helper_spec.rb
index 43811a0d..fb5ddd0b 100644
--- a/spec/trestle/helpers/url_helper_spec.rb
+++ b/spec/trestle/helpers/url_helper_spec.rb
@@ -1,10 +1,6 @@
require 'spec_helper'
-require_relative '../../../app/helpers/trestle/url_helper'
-
-describe Trestle::UrlHelper do
- include Trestle::UrlHelper
-
+describe Trestle::UrlHelper, type: :helper do
let(:form) { double(modal?: false) }
let(:admin) { double(form: form) }
let(:modal_request?) { false }