From d513db92851674cdb858c2c49ddf114311974380 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Thu, 17 Sep 2026 01:58:42 +0100 Subject: [PATCH] FIX: Wait for the page to load before warning that no preference platform was found A preference platform's bundle loads asynchronously, so on a first visit it has not registered when this script constructs. The script warned right then, which read as an integration error on a page that was correctly integrated, and the answer arrived moments later on the window event and everything worked. The warning now waits the way a platform waits for this script's object, ending at the page's load event or after 5000 milliseconds, whichever comes first, and skipping the wait where the page has already loaded. An answer from any source during the wait, or a platform that registers during it, means nothing is printed. Where no platform ever appears the warning is printed exactly as before, because that case matters. Rendered output with the user prompt block off is byte for byte what it was, and with the block on the only change is this function. Checks in tests/template-tests.js went from 157 to 176, all passing. --- JavaScriptResource.mustache | 73 ++++++++++++-- tests/template-tests.js | 191 +++++++++++++++++++++++++++++++++++- 2 files changed, 256 insertions(+), 8 deletions(-) diff --git a/JavaScriptResource.mustache b/JavaScriptResource.mustache index 565739a..cafb55e 100644 --- a/JavaScriptResource.mustache +++ b/JavaScriptResource.mustache @@ -664,16 +664,77 @@ fiftyoneDegreesManager = function() { } }; - // Where no platform was found at construction and no source had an - // answer there is no recovery on this page view, and this warning is the - // only signal. An answer already read from storage means a 51Did will be - // created, so saying otherwise would be wrong and the warning is not - // printed. No value is ever printed by this block. - var warnIfNoPlatform = function() { + // Where no platform ever appears and no source has an answer there is no + // recovery on this page view, and this warning is the only signal, so the + // case it covers matters. It is deliberately not printed at construction, + // because a page's platform tag is ordinarily asynchronous and a platform + // that has not registered yet is not a platform that is not coming. An + // answer from any source, including one that arrives during the wait + // below, means a 51Did will be created, so saying otherwise would be + // wrong and nothing is printed. No value is ever printed by this block. + var sayNoPlatform = function() { if (!answer && platformPresent() === false) { console.warn("51Degrees: no preference platform was found on this page. A platform's stub must precede this script. No 51Did will be created until a platform answers."); } }; + + // The wait before that warning, matching the one a platform makes for + // this script's object so that neither side calls the other absent whilst + // it is still on its way. The wait ends at the page's load event, by + // which time every tag the page's markup carries has run, and after 5000 + // milliseconds at the latest for a page whose load event is very late or + // never comes, whichever of the two comes first. Where the page has + // already loaded nothing is waited for, because a platform that is not + // there then is not coming from a tag. This only logs. It changes nothing + // about processing and no request waits for it. + var warnIfNoPlatform = function() { + // Nothing to wait for where there is no page to load, which is the + // server side evaluation that renders this script. + if (typeof window === 'undefined' || window === null || + typeof window.addEventListener !== 'function' || + typeof setTimeout !== 'function') { + sayNoPlatform(); + return; + } + // An answer or a platform already in hand, so there is nothing this + // wait could change. + if (answer || platformPresent()) { + return; + } + try { + if (typeof document !== 'undefined' && document && + document.readyState === 'complete') { + sayNoPlatform(); + return; + } + } catch (err) { + // A document that throws on a read tells us nothing about the + // page's state, so the wait below covers it. + } + // Either end runs this, and only the first of them counts. + var ended = false; + var end = function() { + if (ended === true) { + return; + } + ended = true; + try { + if (typeof window.removeEventListener === 'function') { + window.removeEventListener('load', end, false); + } + } catch (err) { + // There is nothing left to remove. + } + sayNoPlatform(); + }; + try { + window.addEventListener('load', end, false); + } catch (err) { + // The page's load event cannot be heard, so the limit below is + // the only end to the wait. + } + setTimeout(end, 5000); + }; {{/_userPrompt}} {{/_updateEnabled}} diff --git a/tests/template-tests.js b/tests/template-tests.js index 22bedde..183e8a7 100644 --- a/tests/template-tests.js +++ b/tests/template-tests.js @@ -665,9 +665,9 @@ function logged(result, text) { check('the first request carries no answer', view.endpoint.bodies[0].indexOf('id.usage') === -1, view.endpoint.bodies[0]); - check('a page with no platform at construction is warned once', + check('a page still loading is not warned about a missing platform', view.log.filter(e => String(e[1]).indexOf( - '51Degrees: no preference platform') === 0).length === 1, + '51Degrees: no preference platform') === 0).length === 0, JSON.stringify(view.log)); check('no value is printed by the block', !logged(view, 'standard') && !logged(view, 'purple'), @@ -1241,6 +1241,193 @@ function logged(result, text) { JSON.stringify(answered.context.fod.fodid)); } +// --------------------------------------------------------------------------- +section('The wait before the missing platform warning'); +// --------------------------------------------------------------------------- + +// The warning is the only signal a page with no platform gets, so it has to +// survive, but a platform's bundle loads asynchronously and is ordinarily not +// registered when this script constructs. Printing at construction called an +// ordinary integration broken. The wait matches the one a platform makes for +// this script's object, ending at the page's load event or after 5000 +// milliseconds, whichever comes first, and it is skipped where the page has +// already loaded. + +// A setTimeout the test drives, so the 5000 millisecond limit can be reached +// without the test waiting five seconds for it. +function makeClock() { + const pending = []; + return { + setTimeout: function (fn, ms) { + pending.push({ fn: fn, ms: ms }); + return pending.length; + }, + clearTimeout: function () {}, + pending: pending, + // Run every timer set for this delay, in the order they were set. + fire: function (ms) { + const due = pending.filter(t => t.ms === ms); + due.forEach(t => t.fn()); + return due.length; + } + }; +} + +function platformWarnings(view) { + return view.log.filter(e => e[0] === 'warn' && String(e[1]).indexOf( + '51Degrees: no preference platform') === 0); +} + +const noPlatformModel = { _userPrompt: true, _parameters: '{"mark":"one"}' }; + +{ + // 20. The page loads with no platform on it, which is the case the + // warning is for, and the load event is what ends the wait. + const view = pageView(makeTab(), { + model: noPlatformModel, responses: [secondPayload] + }); + await settle(12); + check('nothing is said whilst the page is still loading', + platformWarnings(view).length === 0, JSON.stringify(view.log)); + check('the wait is held on the page\'s load event', + view.context.__listenerCount('load') === 1, + 'listeners ' + view.context.__listenerCount('load')); + + view.context.__fire('load'); + await settle(4); + check('a page that loads with no platform is warned once', + platformWarnings(view).length === 1, JSON.stringify(view.log)); + check('the warning is the one the page always got', + platformWarnings(view)[0][1] === + '51Degrees: no preference platform was found on this page. ' + + 'A platform\'s stub must precede this script. No 51Did will be ' + + 'created until a platform answers.', + JSON.stringify(platformWarnings(view))); + check('the load listener is removed once the wait has ended', + view.context.__listenerCount('load') === 0, + 'listeners ' + view.context.__listenerCount('load')); + + view.context.__fire('load'); + await settle(4); + check('a second load event says it again no more than once', + platformWarnings(view).length === 1, JSON.stringify(view.log)); +} + +{ + // 21. The limit ends the wait for a page whose load event is very late or + // never comes, and it is the same 5000 milliseconds a platform waits + // for this script. + const clock = makeClock(); + const view = pageView(makeTab(), { + model: noPlatformModel, responses: [secondPayload], + globals: { setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout } + }); + await settle(2); + check('the wait is bounded at 5000 milliseconds', + clock.pending.filter(t => t.ms === 5000).length === 1, + JSON.stringify(clock.pending.map(t => t.ms))); + check('nothing is said before that limit is reached', + platformWarnings(view).length === 0, JSON.stringify(view.log)); + + clock.fire(5000); + check('a page whose load event never comes is warned at the limit', + platformWarnings(view).length === 1, JSON.stringify(view.log)); + + view.context.__fire('load'); + check('a load event after the limit does not say it twice', + platformWarnings(view).length === 1, JSON.stringify(view.log)); +} + +{ + // 22. An answer that arrives during the wait means a 51Did is created, so + // there is nothing to warn about and nothing is printed. + const view = pageView(makeTab(), { + model: noPlatformModel, responses: [secondPayload, didPayload] + }); + await settle(12); + view.context.__fire('51d-pmp-preference', { preference: 'standard' }); + await settle(12); + view.context.__fire('load'); + await settle(4); + check('an answer arriving during the wait stops the warning', + platformWarnings(view).length === 0, JSON.stringify(view.log)); + check('that answer still reached the request', + view.endpoint.bodies.length === 2 && + view.endpoint.bodies[1].indexOf('id.usage=standard') !== -1, + JSON.stringify(view.endpoint.bodies)); +} + +{ + // 23. A platform that registers during the wait, which is the ordinary + // asynchronous integration and the case that produced the noise. + const framework = makeFramework(null); + const view = pageView(makeTab(), { + model: noPlatformModel, responses: [secondPayload, didPayload] + }); + await settle(12); + view.context.__tcfapi = framework.api; + view.context.__fire('load'); + await settle(4); + check('a platform that registers during the wait stops the warning', + platformWarnings(view).length === 0, JSON.stringify(view.log)); +} + +{ + // 24. A page that has already loaded when this script runs waits for + // nothing, because a platform that is not there then is not coming + // from a tag the markup carries. + const clock = makeClock(); + const view = pageView(makeTab(), { + model: noPlatformModel, responses: [secondPayload], + globals: { + document: { readyState: 'complete' }, + setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout + } + }); + await settle(2); + check('a page that has already loaded is warned at once', + platformWarnings(view).length === 1, JSON.stringify(view.log)); + check('a page that has already loaded waits for nothing', + clock.pending.filter(t => t.ms === 5000).length === 0 && + view.context.__listenerCount('load') === 0, + JSON.stringify(clock.pending.map(t => t.ms)) + ' listeners ' + + view.context.__listenerCount('load')); +} + +{ + // 25. A platform found at construction is never waited for and never + // warned about, which is the behaviour the block always had. + const clock = makeClock(); + const framework = makeFramework(null); + const view = pageView(makeTab(), { + model: noPlatformModel, responses: [secondPayload, didPayload], + globals: { + __tcfapi: framework.api, + setTimeout: clock.setTimeout, clearTimeout: clock.clearTimeout + } + }); + await settle(12); + check('a platform at construction is not waited for', + clock.pending.filter(t => t.ms === 5000).length === 0 && + view.context.__listenerCount('load') === 0, + JSON.stringify(clock.pending.map(t => t.ms)) + ' listeners ' + + view.context.__listenerCount('load')); + check('a platform at construction is never warned about', + platformWarnings(view).length === 0, JSON.stringify(view.log)); +} + +{ + // 26. The wait belongs to the user prompt block alone, so a page rendered + // without the block carries neither the warning nor the limit. + const withoutBlock = render({ _userPrompt: false }); + check('the missing platform warning is only in the user prompt block', + withoutBlock.indexOf('no preference platform') === -1, + 'found at ' + withoutBlock.indexOf('no preference platform')); + check('the 5000 millisecond limit is only in the user prompt block', + withoutBlock.indexOf('5000') === -1, + 'found at ' + withoutBlock.indexOf('5000')); +} + // --------------------------------------------------------------------------- section('An answer known at construction, with no snippet that saves a value');