Add optional client-side dismissal SKU for closable billboards (#20716)

* Add optional client-side dismissal SKU for closable billboards

* Safe nav
This commit is contained in:
Ben Halpern 2024-03-01 16:13:19 -05:00 committed by GitHub
parent ac4dcd9907
commit f2968baadb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 108 additions and 34 deletions

View file

@ -48,7 +48,7 @@ module Api
def permitted_params
params.permit :approved, :body_markdown, :creator_id, :display_to,
:name, :organization_id, :placement_area, :published,
:name, :organization_id, :placement_area, :published, :dismissal_sku,
:tag_list, :type_of, :exclude_article_ids, :weight, :requires_cookies,
:audience_segment_type, :audience_segment_id, :priority, :special_behavior,
:custom_display_label, :template, :render_mode, :preferred_article_ids,

View file

@ -12,6 +12,27 @@ describe('getBillboard', () => {
<div class="js-billboard-container" data-async-url="/billboards/sidebar_left_2"></div>
</div>
`;
// Mock localStorage
const localStorageMock = (function() {
let store = {};
return {
getItem: function(key) {
return store[key] || null;
},
setItem: function(key, value) {
store[key] = value.toString();
},
clear: function() {
store = {};
},
removeItem: function(key) {
delete store[key];
}
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
});
});
afterEach(() => {
@ -66,7 +87,6 @@ describe('getBillboard', () => {
});
test('should clone and re-insert script tags in fetched content', async () => {
// Setup fetch to return a script tag
global.fetch = jest.fn(() =>
Promise.resolve({
text: () =>
@ -78,25 +98,17 @@ describe('getBillboard', () => {
await getBillboard();
// Find the new script tags in the document
const scriptElements = document.querySelectorAll(
'.js-billboard-container script',
);
// Assert that the script tags are properly cloned and re-inserted
expect(scriptElements.length).toBe(2); // You have two .js-billboard-container in your setup, so expect two script tags
const scriptElements = document.querySelectorAll('.js-billboard-container script');
expect(scriptElements.length).toBe(2);
scriptElements.forEach((script) => {
expect(script.type).toEqual('text/javascript'); // Should retain attributes
expect(script.innerHTML).toEqual('console.log("test")'); // Should retain content
expect(script.type).toEqual('text/javascript');
expect(script.innerHTML).toEqual('console.log("test")');
});
});
test('should add current URL parameters to asyncUrl if bb_test_placement_area exists', async () => {
// Mocking window.location.href
delete window.location;
window.location = new URL(
'http://example.com?bb_test_placement_area=post_sidebar&bb_test_id=1',
);
window.location = new URL('http://example.com?bb_test_placement_area=post_sidebar&bb_test_id=1');
document.body.innerHTML = `
<div>
@ -112,10 +124,37 @@ describe('getBillboard', () => {
await getBillboard();
// Check if the fetch function was called with the modified URL including the parameters
expect(global.fetch).toHaveBeenCalledWith(
'/billboards/post_sidebar?bb_test_placement_area=post_sidebar&bb_test_id=1',
expect(global.fetch).toHaveBeenCalledWith('/billboards/post_sidebar?bb_test_placement_area=post_sidebar&bb_test_id=1');
});
test('should display none if dismissal SKU matches', async () => {
window.localStorage.setItem('dismissal_skus_triggered', JSON.stringify(['sku123']));
global.fetch = jest.fn(() =>
Promise.resolve({
text: () => Promise.resolve('<div class="js-billboard" data-dismissal-sku="sku123">Billboard Content</div>'),
}),
);
await getBillboard();
const billboardContent = document.querySelector('.js-billboard-container div');
expect(billboardContent.closest('.js-billboard-container').style.display).toBe('none');
});
test('should display billboard content if there is no matching dismissal SKU', async () => {
window.localStorage.setItem('dismissal_skus_triggered', JSON.stringify(['sku999']));
global.fetch = jest.fn(() =>
Promise.resolve({
text: () => Promise.resolve('<div class="js-billboard" data-dismissal-sku="sku123">Billboard Content</div>'),
}),
);
await getBillboard();
const billboardContent = document.querySelector('.js-billboard-container div');
expect(billboardContent.closest('.js-billboard-container').style.display).toBe(''); // Not marked as display none
});
});
@ -141,10 +180,7 @@ describe('executeBBScripts', () => {
test('should skip null or undefined script elements', () => {
container.innerHTML = '<script>window.someGlobalVar = "executed";</script>';
// Simulating an inconsistency in the returned HTMLCollection
const spiedGetElementsByTagName = jest
.spyOn(container, 'getElementsByTagName')
.mockReturnValue([null, undefined]);
const spiedGetElementsByTagName = jest.spyOn(container, 'getElementsByTagName').mockReturnValue([null, undefined]);
executeBBScripts(container);
@ -152,8 +188,7 @@ describe('executeBBScripts', () => {
});
test('should copy attributes of original script element', () => {
container.innerHTML =
'<script type="text/javascript" async>window.someGlobalVar = "executed";</script>';
container.innerHTML = '<script type="text/javascript" async>window.someGlobalVar = "executed";</script>';
executeBBScripts(container);
@ -171,8 +206,7 @@ describe('executeBBScripts', () => {
});
test('should insert the new script element at the same position as the original', () => {
container.innerHTML =
'<div></div><script>window.someGlobalVar = "executed";</script><div></div>';
container.innerHTML = '<div></div><script>window.someGlobalVar = "executed";</script><div></div>';
executeBBScripts(container);

View file

@ -23,7 +23,7 @@ async function generateBillboard(element) {
}
if (cookieStatus === 'allowed') {
asyncUrl += `${asyncUrl.includes('?') ? '&' : '?' }cookies_allowed=true`;
asyncUrl += `${asyncUrl.includes('?') ? '&' : '?'}cookies_allowed=true`;
}
if (asyncUrl) {
@ -39,6 +39,15 @@ async function generateBillboard(element) {
this.style.display = 'none';
};
});
const dismissalSku =
element.querySelector('.js-billboard')?.dataset.dismissalSku;
if (localStorage && dismissalSku && dismissalSku.length > 0) {
const skuArray =
JSON.parse(localStorage.getItem('dismissal_skus_triggered')) || [];
if (skuArray.includes(dismissalSku)) {
element.style.display = 'none';
}
}
executeBBScripts(element);
implementSpecialBehavior(element);
setupBillboardInteractivity();

View file

@ -6,7 +6,7 @@ describe('billboard close functionality', () => {
// Setup a simple DOM structure that includes only the elements needed for the close functionality
document.body.innerHTML = `
<div class="another-element"></div>
<div class="js-billboard" style="display: block;">
<div class="js-billboard" style="display: block;" data-dismissal-sku="WHATUP">
<button id="sponsorship-close-trigger-1"></button>
</div>
`;
@ -35,4 +35,16 @@ describe('billboard close functionality', () => {
const billboard = document.querySelector('.js-billboard');
expect(billboard.style.display).toBe('none');
});
it('adds dismissal sku to local storage when dismissing a billboard', () => {
setupBillboardInteractivity();
// Simulate clicking the close button
const closeButton = document.querySelector('#sponsorship-close-trigger-1');
closeButton.click();
// Assert the dismissal sku is added to local storage
const dismissalSkus = JSON.parse(localStorage.getItem('dismissal_skus_triggered'));
expect(dismissalSkus).toEqual(['WHATUP']);
});
});

View file

@ -29,11 +29,11 @@ export function setupBillboardInteractivity() {
if (sponsorshipCloseButtons.length) {
sponsorshipCloseButtons.forEach((sponsorshipCloseButton) => {
sponsorshipCloseButton.addEventListener('click', () => {
sponsorshipCloseButton.closest('.js-billboard').style.display = 'none';
dismissBillboard(sponsorshipCloseButton);
});
document.addEventListener('click', (event) => {
if (!event.target.closest('.js-billboard')) {
sponsorshipCloseButton.closest('.js-billboard').style.display = 'none';
dismissBillboard(sponsorshipCloseButton);
}
});
});
@ -51,3 +51,15 @@ function amendBillboardStyle(sponsorshipDropdownButton) {
'revert';
}
}
function dismissBillboard(sponsorshipCloseButton) {
const sku = sponsorshipCloseButton.closest('.js-billboard').dataset.dismissalSku;
sponsorshipCloseButton.closest('.js-billboard').style.display = 'none';
if (localStorage && sku && sku.length > 0) {
const skuArray = JSON.parse(localStorage.getItem('dismissal_skus_triggered')) || [];
if (!skuArray.includes(sku)) {
skuArray.push(sku);
localStorage.setItem('dismissal_skus_triggered', JSON.stringify(skuArray));
}
}
}

View file

@ -49,6 +49,7 @@
data-category-click="<%= BillboardEvent::CATEGORY_CLICK %>"
data-category-impression="<%= BillboardEvent::CATEGORY_IMPRESSION %>"
data-context-type="<%= data_context_type %>"
data-dismissal-sku="<%= billboard.dismissal_sku %>"
data-special="<%= billboard.special_behavior %>"
data-article-id="<%= @article&.id %>"
data-type-of="<%= billboard.type_of %>">

View file

@ -0,0 +1,5 @@
class AddDismissalSkuToBillboards < ActiveRecord::Migration[7.0]
def change
add_column :display_ads, :dismissal_sku, :string
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_02_27_181740) do
ActiveRecord::Schema[7.0].define(version: 2024_03_01_160047) do
# These are extensions that must be enabled in order to support this database
enable_extension "citext"
enable_extension "ltree"
@ -479,6 +479,7 @@ ActiveRecord::Schema[7.0].define(version: 2024_02_27_181740) do
t.datetime "created_at", precision: nil, null: false
t.integer "creator_id"
t.string "custom_display_label"
t.string "dismissal_sku"
t.integer "display_to", default: 0, null: false
t.integer "exclude_article_ids", default: [], array: true
t.integer "impressions_count", default: 0

View file

@ -51,7 +51,7 @@ RSpec.describe "Api::V1::Billboards" do
"impressions_count", "name", "organization_id",
"placement_area", "processed_html", "published",
"success_rate", "tag_list", "type_of", "updated_at",
"creator_id", "exclude_article_ids",
"creator_id", "exclude_article_ids", "dismissal_sku",
"audience_segment_type", "audience_segment_id",
"custom_display_label", "template", "render_mode", "preferred_article_ids",
"priority", "weight", "target_geolocations", "requires_cookies", "special_behavior")
@ -71,7 +71,7 @@ RSpec.describe "Api::V1::Billboards" do
"impressions_count", "name", "organization_id",
"placement_area", "processed_html", "published",
"success_rate", "tag_list", "type_of", "updated_at",
"creator_id", "exclude_article_ids",
"creator_id", "exclude_article_ids", "dismissal_sku",
"audience_segment_type", "audience_segment_id",
"custom_display_label", "template", "render_mode", "preferred_article_ids",
"priority", "weight", "target_geolocations", "requires_cookies", "special_behavior")
@ -135,7 +135,7 @@ RSpec.describe "Api::V1::Billboards" do
contain_exactly("approved", "body_markdown", "cached_tag_list",
"clicks_count", "created_at", "display_to", "id",
"impressions_count", "name", "organization_id",
"placement_area", "processed_html", "published",
"placement_area", "processed_html", "published", "dismissal_sku",
"success_rate", "tag_list", "type_of", "updated_at",
"creator_id", "exclude_article_ids", "requires_cookies",
"audience_segment_type", "audience_segment_id", "special_behavior",