Provide Default Icon for editor_button placement

Tools with the editor_button placement now have the right to remain
silent as to an icon_url. Tools have the right to an icon; if they
cannot afford an icon, one will be appointed for them.

closes INTEROP-8426
flag=allow_lti_tools_editor_button_placement_without_icon

Test plan:
- Ensure the feature flag is on (default in dev)
- Create LTI tools with no icon_url at the top level or in the editor
  button placement. You can try some of the following for the name of
  the tool (or 'title' in LTI 1.3).
  (Or you can test some of these out directly with the
  /lti/tool_default_icon endpoint, with the name parameter)
  - titles that start with some punctuation/whitespace
  - titles which are all punctuation/whitespace
  - titles with non-ASCII characters, e.g. ė, अ, 好, or 👍
  - at least one simple title starting with a letter
- Make at least one of the apps a favorite in the RCE (slider button in
  the apps list in the course/account settings)
- Make sure the new default icons appear in all three locatons:
  - in the RCE favorite location
  - RCE dropdown
  - RCE "All Apps" listing (accessed from the dropdown)
- Check that the default icons use the first number or letter-like
  (alphabetic, Chinese character, etc.) character in the tool editor
  button name, capitalized if applicable
- Optional: Go to /lti/tool_default_icon?id=123&name=abc in Chrome,
  inspect element on the text, go to "Computed" and at the bottom
  "Rendered Fonts", and make sure "Inter Black" was rendered.
- Optional: Install the same LTI 1.3 tool with no icon_url multiple
  times in one context and make sure both copies have identical icons.
- rune 'rake doc:api' and check that the docs at
  doc/api/file.editor_button_placement.html have been updated to reflect
  the new icon_url behavior.
- Turn the feature flag off
- Edit an LTI 1.3 developer key 9that has at least one installation) to
  have an editor_button placement but not have any icon_url or
  canvas_icon_class in the editor_button placement configuration or top
  level configuration.
- Run jobs and check that tool installations do not have an
  editor_button placement.

Change-Id: I25f83c0995347ab0df887c844139ee74b814a571
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/340408
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Paul Gray <paul.gray@instructure.com>
QA-Review: Paul Gray <paul.gray@instructure.com>
Product-Review: Evan Battaglia <ebattaglia@instructure.com>
This commit is contained in:
Evan Battaglia 2024-02-13 09:06:09 +00:00
parent 2bb315122e
commit 975ae3b313
11 changed files with 268 additions and 20 deletions

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
module Lti
class ToolDefaultIconController < ApplicationController
# Generates an SVG icon for a tool based on its name and ID
# NOTE! If we ever change this file or the template, we'll need to
# bust users' caches by changing the route in routes.rb or adding a
# "version" parameter or similar to the URL. See
# ContextExternalTool#default_icon_path for usage
CACHE_MAX_AGE = 1.month.seconds
COLORS = %w[#fb5607 #3a86ff #5f9207 #8338ec #d81159 #390099 #9e0059].freeze
def show
# Use first number/"letter-like" character ('0', 'a', 'é', '我', etc.), or none.
@glyph = params[:name]&.match(/[0-9\p{Letter}]/)&.to_s&.upcase
# Color based on hash of the developer key / tool (global) ID.
@color = COLORS[params[:id].hash % COLORS.length]
response.headers["Cache-Control"] = "max-age=#{CACHE_MAX_AGE}"
cancel_cache_buster
render content_type: "image/svg+xml", layout: false
end
end
end

View File

@ -539,9 +539,14 @@ module ApplicationHelper
# force the YAML to be deserialized before caching, since it's expensive
tools.each(&:settings)
end
# Default tool icons need to have a hostname (cannot just be a path)
# because they are used externally via ServicesApiController endpoints
default_icon_base_url = "#{request.protocol}#{request.host_with_port}"
ContextExternalTool
.shard(@context.shard)
.editor_button_json(cached_tools.dup, @context, @current_user, session)
.editor_button_json(cached_tools.dup, @context, @current_user, session, default_icon_base_url)
end
def nbsp

View File

@ -224,18 +224,26 @@ class ContextExternalTool < ActiveRecord::Base
false
end
def editor_button_json(tools, context, user, session = nil)
def editor_button_json(tools, context, user, session, default_tool_icon_base_url)
tools.select! { |tool| visible?(tool.editor_button["visibility"], user, context, session) }
markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML.new({ link_attributes: { target: "_blank" } }))
always_on_ids = Setting.get("rce_always_on_developer_key_ids", "").split(",").map(&:to_i)
tools.map do |tool|
canvas_icon_class = tool.editor_button(:canvas_icon_class)
icon_url = tool.editor_button(:icon_url)
if canvas_icon_class.blank? && icon_url.blank?
# Default tool icons are served by canvas; some users of this method
# may need a full URL rather than path.
icon_url = default_tool_icon_base_url + tool.default_icon_path
end
{
name: tool.label_for(:editor_button, I18n.locale),
id: tool.id,
favorite: tool.is_rce_favorite_in_context?(context),
url: tool.editor_button(:url),
icon_url: tool.editor_button(:icon_url),
canvas_icon_class: tool.editor_button(:canvas_icon_class),
icon_url:,
canvas_icon_class:,
width: tool.editor_button(:selection_width),
height: tool.editor_button(:selection_height),
use_tray: tool.editor_button(:use_tray) == "true",
@ -827,7 +835,9 @@ class ContextExternalTool < ActiveRecord::Base
settings.delete(type) unless extension_setting(type, :url)
end
unless root_account.feature_enabled?(:allow_lti_tools_editor_button_placement_without_icon)
settings.delete(:editor_button) unless editor_button(:icon_url) || editor_button(:canvas_icon_class)
end
sync_placements!(Lti::ResourcePlacement::PLACEMENTS.select { |type| settings[type] }.map(&:to_s))
true
@ -1587,6 +1597,15 @@ class ContextExternalTool < ActiveRecord::Base
ContextExternalTool.associated_1_1_tool(self, context, launch_url || url || domain)
end
# Icon for tools which don't provide one, based on the DeveloperKey or tool
# id, and the tool name
def default_icon_path
Rails.application.routes.url_helpers.lti_tool_default_icon_path(
id: global_developer_key_id || global_id,
name:
)
end
private
# Locally and in OSS installations, this can be configured in config/dynamic_settings.yml.

View File

@ -0,0 +1,23 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<style type="text/css">
@font-face {
font-family: 'Inter';
font-weight: 1000;
/* Contains only A-Z */
src: url(data:font/woff2;base64,d09GMgABAAAAAAfMAA8AAAAAD/gAAAdxAAME3QAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbhTQcKgZgP1NUQVREADQRCAqQDIwIATYCJANsCzgABCAFg1YHIBu3DIgehU3ZM3bCI6ltNcnHBB0BtMaavVtENUSSaYYGmQTNPIqGBilrKDRrJEKU/zUAN+2SICGkIZWJ8xOr8asZTA2dVgwq9nSVtM6MMlNnqk87E2uAf//387/m6h36iZPXsSudR2iETCnfzmaOqdelhQZtCXHveNPQNWkphBLYxK2p5yDVK1nJnBe9EwGUA8gQCI0RCFCqtU7I6e59deDW4q1Z4IiA34t6q8l5WaBBCyYioyXCGBLwxZPhnQjhJwuEF9bQIFWU0+IKZX2F9RO9GJLqvyBthv/X/vIizIM5WlrozVtOKnBC7AiT3hm1tUYLiij3vZnFnUIvQZR6fbRu5t/FrVy9Q7slL0vt+MtyiU8tsmT7/y5245vtwgu3p3vY90CeJEn9+dEDb4MkRSNQPLkBIpYCEPAIMgwxEXce5CQ4z3gKwSZikhyhU8iTEgMDK9HTn9mobbUPbNhsWDsW+VE2iKFoUDIX8Ioh+RucVG5GnCPpJwIhXnJhsZ8WTmgMLV392wRUpyZXA+EOoBANdGMRCDQS1AdM/1TbqWYroQDW17KwFA+CSDgLCD2UDXDiVUIDgPQFZFbMgV0YFSJKJhJdu3Qm00QatM6IvHRko6/3fG2AIdALWSmQHwEgAr+eatoYcdwH5ZqyWOTFMG3lcpbpwIo13CQ8y/MNaUgWBdL18Xgcwyg4QVHEFTNWT9JDLrjX3K9FhculQ21tutS1e0yKW81VorKujmEpeYrTZIYROB3lkp0GUIHN575qzHRdc91jHA+eUNVPH7rV3K+97s4L7hPuXyerrt3Lm4vSsnJBIHnSHUTZk0RY+sRcQs9S7nMltk2+QVXeTH3kS+jlgkKikCqKOCknEW4mQRAl10OAsZuMCk+SaowUBFJhHZOmEwkxRe5ki8PqnIwpZIsZmptL743diuQyoerRPXbjA0y4/zincivkpWivX0YSR7kdRCqaPWj0ycnsUEOZXsQVC4+6ms7fkpr0ekC5znVmw9DjmFotpJQ2GB48MdbdfAUwyRcogeSOrqugSoRooF+bRSiKXVyxYkSS3k0rwxQKFnn6leQzZk4oLrZVKOLEckrgKYEsdCdnmzr2xDf4x03t4nO8/zLzYjNsoBV+R5cufTw/N2HPlpJRDbyqRVeLy+umrN4o1sy4O2ufyFFjTu2pSSwM0LOmR72yfaMNMbFjk8yvUP6MwIJunXJNg0t9BjqcYxN+b92f9OtQ2dh+RW2H9Ao1yzQBlKyylH3yQDv+fuOD78lanUHqOvTO60g3y+czM9phthdo7PC5Tz/OqNXfz5z+1OKqa3ZdfM7burq8t7fiFx+DDbTh43DOjuvr7Bs33WkAQ90/a1xTZ1Mlhq0MPxYycHBoRlON49jolJ9bt8f/PTx2cvnqXtqxw2NlLoejacNXjoVXWO+jv3/Vdv0zd6wnZ7Ff3Bma8m7/NEIxBzbQfLrvKX0QwBuXCABGzt6a+AAnT/gNX/3weLdxh9vipQ/Z18+iC4yK1gV5p6H/E/Yw3LRtdeQ47y6TfbrmzD77d9d4+zIk45FO0PPY3uzYuIYw1XbQlPd8U9vknnABs/nVShQUA8Uu+XUNZAN9QLvma1PxKti6wlDiOvqufsMjH2iXzkC7jrxrWF+fSuaK0O+O+7W/ym4PIXbfLZMl/0KsX/ptB7rduHVEM3LDC37z4gR7197lRlPvEnvnhGOQTCPs4dvmeUel3sZFrz42WfYylJy8jmLFgRC178g5ClXvdFWWPbBa+y7rldT64S3uzwj9ZiqkibbssYaM9gHdWK8XXfwsbXpyq1qtafU+f87Cnsdhdfpkg7wpIrljwY2eQHUcoS2frUkbUuAd1Y/gwruPDs1teNZbER7qhbc4cqX1qGUfv1lWHPPBKYhe1M5sNZxmSMZbw1LQFcOHuPvFHd2E3VIZxNzSvVCn8nkXq50+XRsbN02LNWPhPY/GM8t50Fu/EUvgss0pZNTYXvFex149C03veNn8LsOW49VwrJCHLcH3FAMWea02o21XtHtCyXIsnyxb2K4r2kYgL0ZGC6Xmwz1k+ItLbO1tcARGjmaPOc0VEO4KgADyeHtExyOLYxWRn+kmcgAfH/Y1AfjUD6z7/u23qmka2xhQiPSi/26mjhMn+BPoAYRhu21ln4KadkJdZIKNtqKkmIdNcg1MGdDZCkYdRAXmFKJF0S2UeoaAUZukn0LkpygpCmWOYf9ZpDoMLHKgb9E7129Tl9NQFSdBV2yGqmg5HCBAm1gSscR70eZtbBm71CDKALeAzBHqm8yROCs5io/t3UUUTizYvXCJ+qF+BUIBNviEHA8mhHZrwHlK4h8woTO6yJajSB6LVGZWSp2oIEZJZ2GTc3rJYpVWy6M0UJ5saZIlklfXyGdlli3Pf5Tas8RY5Wgx4fzH2FQWwiryJfCTKFsmf7KUCSgyWth7dlhp9c4yxEuUbrBkqfL1j84TwI/Kv8JEGEyrH60Iz4i+jIFE3i2yZVG2dF8lfanEC7KNjUqwZCoJXIxRfElarRWRmrutlCG6/yo4AQAAAA==) format('woff2');
unicode-range: U+0000-00FF;
}
</style>
</defs>
<rect width="24" height="24" rx="2" fill="<%= @color %>" />
<path
fill-rule="evenodd" clip-rule="evenodd" fill="white" fill-opacity="0.4"
d="M19.6863 9.17647C19.6863 11.9012 17.4699 14.1176 14.7452 14.1176H11.9216C9.19692 14.1176 6.98045 11.9012 6.98045 9.17647V7.05882H19.6863V9.17647ZM22.5098 5.64706H21.0981H18.2745V0H16.8628V5.64706H9.80396V0H8.3922V5.64706H5.56867H4.1569V7.05882H5.56867V9.17647C5.56867 12.6791 8.41902 15.5294 11.9216 15.5294H12.6275V20.4706C12.6275 21.6381 11.6774 22.5882 10.5098 22.5882C9.34231 22.5882 8.3922 21.6381 8.3922 20.4706C8.3922 18.5252 6.8082 16.9412 4.86279 16.9412C2.91737 16.9412 1.33337 18.5252 1.33337 20.4706V24H2.74514V20.4706C2.74514 19.3031 3.69526 18.3529 4.86279 18.3529C6.03031 18.3529 6.98043 19.3031 6.98043 20.4706C6.98043 22.416 8.56443 24 10.5098 24C12.4567 24 14.0393 22.416 14.0393 20.4706V15.5294H14.7451C18.2477 15.5294 21.0981 12.6791 21.0981 9.17647V7.05882H22.5098V5.64706Z"
/>
<text x="50%" y="57%" dominant-baseline="middle" text-anchor="middle"
font-size="20" font-family="Inter, Arial, sans-serif" font-weight="1000"
fill="white"><%= @glyph %></text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -236,3 +236,21 @@ assignment_edit_placement_not_on_announcements:
state: allowed_on
ci:
state: allowed_on
allow_lti_tools_editor_button_placement_without_icon:
state: hidden
applies_to: RootAccount
display_name: Allow LTI tools to have an editor button icon without placement (use default tool icon)
description: |-
When disabled, new or updated ContextExternalTool installations (including
LTI 1.3 tools updated automatically due to updates to their developer keys)
will have their editor_button placement removed if there is no icon_url or
canvas_icon_class provided in the settings (legacy behavior). When enabled,
the placement is not removed, so tools without icons will show a default
icon (with a color / letter based on tool name and tool/developer key ID).
environments:
development:
state: allowed_on
test:
state: allowed_on
ci:
state: allowed_on

View File

@ -2670,6 +2670,8 @@ CanvasRails::Application.routes.draw do
get "post_message_forwarding", controller: "lti/platform_storage", action: :post_message_forwarding, as: :lti_post_message_forwarding
get "lti/tool_default_icon" => "lti/tool_default_icon#show"
ApiRouteSet.draw(self, "/api/lti/v1") do
post "tools/:tool_id/grade_passback", controller: :lti_api, action: :grade_passback, as: "lti_grade_passback_api"
post "tools/:tool_id/ext_grade_passback", controller: :lti_api, action: :legacy_grade_passback, as: "blti_legacy_grade_passback_api"

View File

@ -69,12 +69,19 @@ All of these settings are contained for the **editor_button** placement:
button. This can be overridden by language-specific settings if desired by
using the labels setting. This is required if a text value is not set on the main tool configuration.
- icon_url &lt;url&gt; (required if not set on main tool configuration)
- icon_url &lt;url&gt; (optional)
The URL for an icon that identifies your tool in the RCE toolbar. It is
recommended that this icon be at least 16x16 px,in PNG or SVG format, and
The url must be an https (SSL) URL. This setting is required if icon_url is
not set on the main tool configuration.
The URL for an icon that identifies your tool in the RCE toolbar. The icon
will be shown at 16x16 pixels in the editor toolbar, and at 28x28 pixels in
the editor's listing of all tools. It is recommended that this icon be in
PNG or SVG format. The url must be an https (SSL) URL.
After April 2024, if a tool does not provide an icon_url on the
editor_button placement or the main tool configuration, a default icon
based on the first letter of the tool's name will be used. Before this
change, if a tool does not provide an icon_url, the editor_button placement
will be removed from the tool's install configuration, and the tool will not
be shown in the editor_button placement.
- labels: &lt;set of locale-label pairs&gt; (optional)

View File

@ -197,8 +197,9 @@ All of these settings are configurable for the "editor_button" placement in the
- icon_url: &lt;url&gt; (optional)
This is the URL of the icon that will be shown on the button in the rich editor. Icons should be 16x16 in size, and can be any standard web image format (png, gif, ico, etc.). It is recommended that this URL be over SSL (https).
This is required if an icon_url is not set on the main tool configuration.
The URL for an icon that identifies your tool in the RCE toolbar. The icon will be shown at 16x16 pixels in the editor toolbar, and at 28x28 pixels in the editor's listing of all tools. It is recommended that this icon be in PNG or SVG format. The url must be an https (SSL) URL.
After April 2024, if a tool does not provide an icon_url on the editor_button placement or the main tool configuration, a default icon based on the first letter of the tool's name will be used. Before this change, if a tool does not provide an icon_url, the editor_button placement will be removed from the tool install configuration, and the tool will not be shown in the editor_button placement.
- text: &lt;text&gt; (optional)

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
#
# Copyright (C) 2024 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
describe Lti::ToolDefaultIconController do
describe "#show" do
render_views
it "generates an SVG icon" do
get :show, params: { name: "test", id: 1 }
expect(response).to have_http_status(:ok)
expect(response.content_type).to eq("image/svg+xml; charset=utf-8")
end
it 'uses the first number/"letter-like" character in the name, capitalized, or none' do
expectations = {
"abc" => "A",
" def" => "D",
"...1a" => "1",
"...我a" => "",
"!!!..." => "",
"😅" => ""
}
expectations.each do |name, expected_glyph|
get :show, params: { name:, id: 1 }
expect(response.body).to include(">#{expected_glyph}</text>")
end
end
it "uses a color based on the hash of the developer key / tool (global) ID" do
id = 1
hash = id.to_s.hash
color = Lti::ToolDefaultIconController::COLORS[hash % Lti::ToolDefaultIconController::COLORS.length]
get :show, params: { name: "test", id: }
expect(response.body).to include("fill=\"#{color}\"")
end
end
end

View File

@ -632,6 +632,20 @@ describe ApplicationHelper do
expect(editor_buttons).to be_empty
end
it "passes in the base url for use with default tool icons" do
@course = course_model
@context = @course
expect(ContextExternalTool).to receive(:editor_button_json).with(
an_instance_of(Array),
anything,
anything,
anything,
"http://test.host"
)
editor_buttons
end
end
describe "UI path checking" do

View File

@ -2565,12 +2565,27 @@ describe ContextExternalTool do
expect(tool.editor_button).to be_nil
tool.settings = { editor_button: { url: "http://www.example.com" } }
tool.save
expect(tool.editor_button).to be_nil
# icon_url now optional, a default will be provided
expect(tool.editor_button).not_to be_nil
tool.settings = { editor_button: { url: "http://www.example.com", icon_url: "http://www.example.com", selection_width: 100, selection_height: 100 } }
tool.save
expect(tool.editor_button).not_to be_nil
end
context "when allow_lti_tools_editor_button_placement_without_icon FF is disabled" do
let(:ff) { :allow_lti_tools_editor_button_placement_without_icon }
before { @root_account.disable_feature! ff }
after { @root_account.enable_feature! ff }
it "deletes the editor_button if icon_url is not present" do
tool = new_external_tool
tool.settings = { editor_button: { url: "http://www.example.com" } }
tool.save
expect(tool.editor_button).to be_nil
end
end
it "sets user_navigation if navigation configured" do
tool = new_external_tool
tool.settings = { user_navigation: { url: "http://www.example.com" } }
@ -3446,41 +3461,86 @@ describe ContextExternalTool do
it "includes a boolean false for use_tray" do
tool.editor_button = { use_tray: "false" }
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym, nil, "")
expect(json[0][:use_tray]).to be false
end
it "includes a boolean true for use_tray" do
tool.editor_button = { use_tray: "true" }
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym, nil, "")
expect(json[0][:use_tray]).to be true
end
it "includes a boolean false for always_on" do
Setting.set("rce_always_on_developer_key_ids", "90000000000001,90000000000002")
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym, nil, "")
expect(json[0][:always_on]).to be false
end
it "includes a boolean true for always_on" do
Setting.set("rce_always_on_developer_key_ids", "90000000000001,#{tool.developer_key.global_id}")
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym, nil, "")
expect(json[0][:always_on]).to be true
end
describe "includes the description" do
it "parsed into HTML" do
tool.description = "the first paragraph.\n\nthe second paragraph."
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym, nil, "")
expect(json[0][:description]).to eq "<p>the first paragraph.</p>\n\n<p>the second paragraph.</p>\n"
end
it 'with target="_blank" on links' do
tool.description = "[link text](http://the.url)"
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym)
json = ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym, nil, "")
expect(json[0][:description]).to eq "<p><a href=\"http://the.url\" target=\"_blank\">link text</a></p>\n"
end
end
describe "icon_url" do
let(:base_url) { "https://myexampleschool.instructure.com" }
def editor_button_icon_url(tool)
ContextExternalTool.editor_button_json([tool], @course, user_with_pseudonym, nil, base_url)[0][:icon_url]
end
it "includes an icon_url when a tool has an top-level icon_url" do
tool.editor_button = {}
tool.settings[:icon_url] = "https://example.com/icon.png"
expect(editor_button_icon_url(tool)).to eq("https://example.com/icon.png")
end
it "includes an icon_url when a tool has an icon_url in editor_button" do
tool.editor_button = { icon_url: "https://example.com/icon.png" }
expect(editor_button_icon_url(tool)).to eq("https://example.com/icon.png")
end
it "doesn't include an icon_url when the tool has a canvas_icon_class and no icon_url" do
tool.editor_button = { canvas_icon_class: "icon_lti" }
expect(editor_button_icon_url(tool)).to be_nil
end
it "uses a default tool icon_url when the tool has no icon_url or canvas_icon_class" do
tool.editor_button = {}
expect(editor_button_icon_url(tool)).to match(
%r{^https://myexampleschool.instructure.com/.*tool_default_icon.*name=editor.thing}
)
end
end
end
describe "#default_icon_path" do
it "references the lti_tool_default_icon_path, tool name, and tool developer key id" do
tool = external_tool_1_3_model(opts: { name: "foo" })
expect(tool.developer_key.global_id).to be_a(Integer)
expect(tool.default_icon_path).to eq("/lti/tool_default_icon?id=#{tool.developer_key.global_id}&name=foo")
end
it "uses tool ID if there is no developer key id" do
tool = external_tool_model(opts: { name: "foo" })
expect(tool.global_id).to be_a(Integer)
expect(tool.default_icon_path).to eq("/lti/tool_default_icon?id=#{tool.global_id}&name=foo")
end
end
describe "is_rce_favorite" do