From 74ab417735c98a58c8d14ae3d472ec34853a3b52 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:10:04 +0200 Subject: [PATCH 01/12] Restore CalorieApp page footer and public CAL market card in maintenance 0.3.24 --- .../workflows/bridge-maintenance-checks.yml | 5 + .../identity-bridge/v1/code-provenance.json | 32 +++ tools/tests/wordpress_page_ending.test.mjs | 119 ++++++++ tools/tests/wordpress_page_ending.test.php | 125 +++++++++ .../calorieapp-identity-bridge/README.md | 13 + .../assets/calorieapp-page-ending.css | 264 ++++++++++++++++++ .../assets/calorieapp-page-ending.js | 221 +++++++++++++++ .../calorieapp-identity-bridge.php | 4 +- ...ieapp-identity-bridge-integrated-login.php | 2 +- ...lorieapp-identity-bridge-market-widget.php | 161 +++++++++++ ...calorieapp-identity-bridge-page-ending.php | 116 ++++++++ .../class-calorieapp-identity-bridge.php | 10 + 12 files changed, 1069 insertions(+), 3 deletions(-) create mode 100644 tools/tests/wordpress_page_ending.test.mjs create mode 100644 tools/tests/wordpress_page_ending.test.php create mode 100644 wordpress-plugins/calorieapp-identity-bridge/assets/calorieapp-page-ending.css create mode 100644 wordpress-plugins/calorieapp-identity-bridge/assets/calorieapp-page-ending.js create mode 100644 wordpress-plugins/calorieapp-identity-bridge/includes/class-calorieapp-identity-bridge-market-widget.php create mode 100644 wordpress-plugins/calorieapp-identity-bridge/includes/class-calorieapp-identity-bridge-page-ending.php diff --git a/.github/workflows/bridge-maintenance-checks.yml b/.github/workflows/bridge-maintenance-checks.yml index f7c711a..7a10373 100644 --- a/.github/workflows/bridge-maintenance-checks.yml +++ b/.github/workflows/bridge-maintenance-checks.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - fix/bridge-033-status-refresh + - fix/desktop-widget-column-fit permissions: contents: read @@ -25,6 +26,10 @@ jobs: run: find wordpress-plugins/calorieapp-identity-bridge -name '*.php' -type f -print0 | xargs -0 -n1 php -l - name: Exercise WordPress page rendering run: php tools/tests/wordpress_site_session_markup.test.php + - name: Check the CalorieApp footer and public market data + run: | + php tools/tests/wordpress_page_ending.test.php + node --test tools/tests/wordpress_page_ending.test.mjs - name: Exercise the preserved login and site-wide session controls run: node --test tools/tests/calorieapp_embed_readiness.test.mjs tools/tests/wordpress_site_session.test.mjs - name: Check responsive header correction lifecycle diff --git a/contracts/identity-bridge/v1/code-provenance.json b/contracts/identity-bridge/v1/code-provenance.json index 8e63215..a43b1da 100644 --- a/contracts/identity-bridge/v1/code-provenance.json +++ b/contracts/identity-bridge/v1/code-provenance.json @@ -145,6 +145,22 @@ "source_file": "calorieappdraft3.ai", "adaptation": "Original phone-mark vector paths and colours retained; surrounding artboard and border omitted." }, + { + "path": "assets/calorieapp-page-ending.css", + "origin_class": "project-repository-material", + "repository_first_add_commit": "pending-current-change", + "declared_distribution_licence": "GPL-2.0-or-later", + "clearance_status": "contributor-source-and-similarity-review-pending", + "source": "CalorieToken/CalorieApp@4baea3acaeaa14dacc602ae8d6154ab4c7e97836; footer and public XPMarket components only; session/layout controllers excluded" + }, + { + "path": "assets/calorieapp-page-ending.js", + "origin_class": "project-repository-material", + "repository_first_add_commit": "pending-current-change", + "declared_distribution_licence": "GPL-2.0-or-later", + "clearance_status": "contributor-source-and-similarity-review-pending", + "source": "CalorieToken/CalorieApp@4baea3acaeaa14dacc602ae8d6154ab4c7e97836; footer and public XPMarket components only; session/layout controllers excluded" + }, { "path": "assets/calorieapp-site-layout.css", "origin_class": "project-repository-material", @@ -223,6 +239,22 @@ "declared_distribution_licence": "GPL-2.0-or-later", "clearance_status": "contributor-and-source-declaration-pending" }, + { + "path": "includes/class-calorieapp-identity-bridge-market-widget.php", + "origin_class": "project-repository-material", + "repository_first_add_commit": "pending-current-change", + "declared_distribution_licence": "GPL-2.0-or-later", + "clearance_status": "contributor-source-and-similarity-review-pending", + "source": "CalorieToken/CalorieApp@4baea3acaeaa14dacc602ae8d6154ab4c7e97836; footer and public XPMarket components only; session/layout controllers excluded" + }, + { + "path": "includes/class-calorieapp-identity-bridge-page-ending.php", + "origin_class": "project-repository-material", + "repository_first_add_commit": "pending-current-change", + "declared_distribution_licence": "GPL-2.0-or-later", + "clearance_status": "contributor-source-and-similarity-review-pending", + "source": "CalorieToken/CalorieApp@4baea3acaeaa14dacc602ae8d6154ab4c7e97836; footer and public XPMarket components only; session/layout controllers excluded" + }, { "path": "includes/class-calorieapp-identity-bridge-rest.php", "origin_class": "project-repository-material", diff --git a/tools/tests/wordpress_page_ending.test.mjs b/tools/tests/wordpress_page_ending.test.mjs new file mode 100644 index 0000000..36f0c74 --- /dev/null +++ b/tools/tests/wordpress_page_ending.test.mjs @@ -0,0 +1,119 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import vm from 'node:vm'; + +const source = readFileSync(new URL('../../wordpress-plugins/calorieapp-identity-bridge/assets/calorieapp-page-ending.js', import.meta.url), 'utf8'); +const tokenUrl = 'https://xpmarket.com/token/Calorie-rNqGa93B8ewQP9mUwpwqA19SApbf62U7PY'; +function element() { + return { + attrs: {}, textContent: '', hidden: false, children: new Map(), listeners: {}, + classList: { add() {} }, + setAttribute(name, value) { this.attrs[name] = String(value); }, + getAttribute(name) { return this.attrs[name] ?? null; }, + querySelector(selector) { return this.children.get(selector) ?? null; }, + addEventListener(name, fn) { this.listeners[name] = fn; }, + set innerHTML(html) { + this.markup = html; + for (const match of html.matchAll(/<[a-z]+\b[^>]*class="([^"]+)"[^>]*>/g)) { + const child = element(); + child.hidden = /\bhidden\b/.test(match[0]); + for (const name of match[1].split(' ')) this.children.set('.' + name, child); + } + }, + }; +} +const good = () => ({ success: true, data: { + code: 'Calorie', issuer: 'rNqGa93B8ewQP9mUwpwqA19SApbf62U7PY', + title: 'Calorie Token', logo: 'https://xpcdn.xpmarket.com/storage/logo/calorie.webp', + price_usd: 0.00000007, price_xrp: 0.00000005, market_cap_usd: 4000, rank: 300, holders: 14000, +} }); +async function settle() { for (let i = 0; i < 10; i++) await Promise.resolve(); } +function run({ response = { ok: true, json: async () => good() }, fetchError, stalled = false, reduced = false, empty = false, endpoint = true } = {}) { + const widget = element(); + const track = element(); + track.clientWidth = 400; + track.children.set('a', { getBoundingClientRect: () => ({ width: 200 }) }); + track.moves = []; + track.scrollBy = (options) => track.moves.push(options); + const carousel = element(); + carousel.children.set('.calorieapp-shared-social-track', track); + const buttons = [-1, 1].map((direction) => { + const button = element(); button.setAttribute('data-calorieapp-carousel-direction', direction); return button; + }); + carousel.querySelectorAll = () => buttons; + const requests = []; + const timers = new Map(); + let ready; + const document = { + readyState: 'loading', + addEventListener(name, fn) { if (name === 'DOMContentLoaded') ready = fn; }, + querySelectorAll(selector) { + if (empty) return []; + return selector.includes('xpmarket-widget') ? [widget] : [carousel]; + }, + }; + const window = { + calorieappPageEnding: endpoint ? { xpMarketWidgetUrl: 'https://calorietoken.net/wp-json/calorieapp/v1/xpmarket-widget', xpMarketTokenUrl: tokenUrl } : {}, + fetch: async (url, options) => { + requests.push({ url, options }); + if (fetchError) throw fetchError; + if (stalled) return new Promise((resolve, reject) => options.signal.addEventListener('abort', () => reject(new Error('aborted')))); + return response; + }, + AbortController, + setTimeout(fn) { timers.set(1, fn); return 1; }, + clearTimeout(id) { timers.delete(id); }, + matchMedia() { return { matches: reduced }; }, + }; + vm.runInNewContext(source, { window, document, URL, Intl }); + ready(); + return { widget, buttons, track, requests, timers, ready }; +} + +test('public CAL data is shown beside an always-available XPMarket destination', async () => { + const h = run(); await settle(); + assert.equal(h.widget.getAttribute('data-state'), 'ready'); + assert.equal(h.widget.querySelector('.calorieapp-xpmarket-price').textContent, '$0.00000007'); + assert.equal(h.widget.querySelector('.calorieapp-xpmarket-link').getAttribute('href'), tokenUrl); + assert.equal(h.widget.querySelector('.calorieapp-xpmarket-state').textContent, 'XPMarket data'); + assert.equal(h.requests[0].options.credentials, 'omit'); + assert.equal(h.timers.size, 0); + h.ready(); await settle(); + assert.equal(h.requests.length, 1, 'Repeat initialization reuses the request.'); +}); +test('failed and incomplete feeds keep the fallback link without inventing prices', async () => { + for (const options of [ + { response: { ok: false } }, { fetchError: new Error('offline') }, + { response: { ok: true, json: async () => ({ success: false }) } }, { endpoint: false }, + ]) { + const h = run(options); await settle(); + assert.equal(h.widget.getAttribute('data-state'), 'fallback'); + assert.equal(h.widget.querySelector('.calorieapp-xpmarket-state').textContent, 'Open XPMarket'); + assert.equal(h.widget.querySelector('.calorieapp-xpmarket-link').getAttribute('href'), tokenUrl); + assert.equal(h.widget.querySelector('.calorieapp-xpmarket-price').textContent, ''); + } +}); +test('unexpected token data is rejected before filling the card', async () => { + const data = good(); data.data.issuer = 'different-token'; + const h = run({ response: { ok: true, json: async () => data } }); await settle(); + assert.equal(h.widget.getAttribute('data-state'), 'fallback'); +}); +test('a stalled market request is abortable and does not leave a permanent loading state', async () => { + const h = run({ stalled: true }); + h.timers.get(1)(); + assert.equal(h.requests[0].options.signal.aborted, true); + await settle(); + assert.equal(h.widget.getAttribute('data-state'), 'fallback'); +}); +test('social arrows move one item and respect reduced motion', () => { + const h = run({ reduced: true }); + h.buttons[0].listeners.click(); h.buttons[1].listeners.click(); + assert.equal(h.track.moves[0].left, -200); + assert.equal(h.track.moves[1].left, 200); + assert.equal(h.track.moves[1].behavior, 'auto'); +}); +test('pages without the CalorieApp ending do not request market data', () => { + const h = run({ empty: true }); + assert.equal(h.requests.length, 0); +}); diff --git a/tools/tests/wordpress_page_ending.test.php b/tools/tests/wordpress_page_ending.test.php new file mode 100644 index 0000000..4297ef6 --- /dev/null +++ b/tools/tests/wordpress_page_ending.test.php @@ -0,0 +1,125 @@ +headers[$name] = $value; } +} +function check($condition, $message): void { if (!$condition) throw new RuntimeException($message); } +function render($object): string { ob_start(); $object->render(); return (string) ob_get_clean(); } +$dir = dirname(__DIR__, 2) . '/wordpress-plugins/calorieapp-identity-bridge/includes/'; +require $dir . 'class-calorieapp-identity-bridge-market-widget.php'; +require $dir . 'class-calorieapp-identity-bridge-page-ending.php'; +use CalorieApp\IdentityBridge\PageEnding; +use CalorieApp\IdentityBridge\MarketWidget; + +$ending = new PageEnding(); +$ending->register_hooks(); +check(isset($actions['wp_footer'][5]), 'Render the ending before footer scripts.'); +$ending->enqueue_assets(); +check(count($styles) === 1 && count($scripts) === 1, 'Only the independent page-ending assets are queued.'); +check($scripts['calorieapp-identity-bridge-page-ending'][1] === [], 'Do not load the authentication controller as a dependency.'); +$html = render($ending); +check(substr_count($html, '