diff --git a/app/controllers/api/v1/billboards_controller.rb b/app/controllers/api/v1/billboards_controller.rb index c4fcdd7cb..09a9b74c2 100644 --- a/app/controllers/api/v1/billboards_controller.rb +++ b/app/controllers/api/v1/billboards_controller.rb @@ -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, diff --git a/app/javascript/__tests__/billboard.test.js b/app/javascript/__tests__/billboard.test.js index 23fd91628..5b878d152 100644 --- a/app/javascript/__tests__/billboard.test.js +++ b/app/javascript/__tests__/billboard.test.js @@ -12,6 +12,27 @@ describe('getBillboard', () => {
`; + // 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 = `
@@ -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('
Billboard Content
'), + }), ); + + 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('
Billboard Content
'), + }), + ); + + 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 = ''; - // 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 = - ''; + container.innerHTML = ''; executeBBScripts(container); @@ -171,8 +206,7 @@ describe('executeBBScripts', () => { }); test('should insert the new script element at the same position as the original', () => { - container.innerHTML = - '
'; + container.innerHTML = '
'; executeBBScripts(container); diff --git a/app/javascript/packs/billboard.js b/app/javascript/packs/billboard.js index e3cc9282f..a9972954f 100644 --- a/app/javascript/packs/billboard.js +++ b/app/javascript/packs/billboard.js @@ -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(); diff --git a/app/javascript/utilities/__tests__/billboardInteractivity.test.js b/app/javascript/utilities/__tests__/billboardInteractivity.test.js index 0878f635c..aeb78e4c6 100644 --- a/app/javascript/utilities/__tests__/billboardInteractivity.test.js +++ b/app/javascript/utilities/__tests__/billboardInteractivity.test.js @@ -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 = `
-
+
`; @@ -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']); + }); }); \ No newline at end of file diff --git a/app/javascript/utilities/billboardInteractivity.jsx b/app/javascript/utilities/billboardInteractivity.jsx index 2f56e1252..b82449560 100644 --- a/app/javascript/utilities/billboardInteractivity.jsx +++ b/app/javascript/utilities/billboardInteractivity.jsx @@ -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)); + } + } +} \ No newline at end of file diff --git a/app/views/shared/_billboard.html.erb b/app/views/shared/_billboard.html.erb index 892ec5c9b..ac113bc72 100644 --- a/app/views/shared/_billboard.html.erb +++ b/app/views/shared/_billboard.html.erb @@ -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 %>"> diff --git a/db/migrate/20240301160047_add_dismissal_sku_to_billboards.rb b/db/migrate/20240301160047_add_dismissal_sku_to_billboards.rb new file mode 100644 index 000000000..a960c5c49 --- /dev/null +++ b/db/migrate/20240301160047_add_dismissal_sku_to_billboards.rb @@ -0,0 +1,5 @@ +class AddDismissalSkuToBillboards < ActiveRecord::Migration[7.0] + def change + add_column :display_ads, :dismissal_sku, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index b4742e0e4..5372b180a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/requests/api/v1/billboards_spec.rb b/spec/requests/api/v1/billboards_spec.rb index 3d05aa5fb..023f093c4 100644 --- a/spec/requests/api/v1/billboards_spec.rb +++ b/spec/requests/api/v1/billboards_spec.rb @@ -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",