Files
sow-nodebb-theme/tests/global-topbar-contract.test.js
T
archvillainetteandClaude Opus 5 4acf32ec09 Make the support drawer core's notification drawer (sow-nodebb-plugin-support#10)
The first version of this drawer built its own rows in JS. This one renders
core's `partials/notifications_list` partial with entries the support plugin
now returns in core's notification shape, so the markup is literally the
notification drawer's: avatar block, stretched link, muted timeago, per-entry
read dot, All/Unread filter row, and the same two footer buttons. The
handlers mirror core's too — only the API paths differ, because the list is
plugin-owned (core's unread count is one aggregate with no hook to carve a
type out of it).

Opening the box no longer marks everything read: like the bell, an entry is
read when you click it or use "Mark all read", and the dot toggles both ways.

Deletes the bespoke row builder and its hand-rolled relative-time helper;
`$.timeago` is what core uses for JS-inserted rows. The dropdown reuses the
`.notifications-dropdown` class this theme already styles, so it needs no CSS
of its own. Hardcoded English is gone (the code-review finding on the first
version): every string is a translation key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 22:20:04 +02:00

241 lines
7.3 KiB
JavaScript

'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
function read(relativePath) {
const filePath = path.join(__dirname, '..', relativePath);
assert(fs.existsSync(filePath), `${relativePath} should exist`);
return fs.readFileSync(filePath, 'utf8');
}
function assertMissing(relativePath, message) {
const filePath = path.join(__dirname, '..', relativePath);
assert(!fs.existsSync(filePath), message || `${relativePath} should not exist`);
}
function assertIncludes(haystack, needle, message) {
assert(
haystack.includes(needle),
message || `Expected file to include ${needle}`
);
}
function assertExcludes(haystack, needle, message) {
assert(
!haystack.includes(needle),
message || `Expected file not to include ${needle}`
);
}
function assertMatches(haystack, pattern, message) {
assert(
pattern.test(haystack),
message || `Expected file to match ${pattern}`
);
}
const header = read('templates/header.tpl');
const footer = read('templates/footer.tpl');
const topbar = read('templates/partials/header/topbar.tpl');
const theme = read('theme.scss');
const stylesheet = read('scss/westgate/_topbar.scss');
const client = read('public/client.js');
const themeHooks = read('lib/theme.js');
assertIncludes(
header,
'<!-- IMPORT partials/header/topbar.tpl -->',
'Header should mount the Westgate topbar partial'
);
assertExcludes(
header,
'<!-- IMPORT partials/sidebar-left.tpl -->',
"Header should no longer render Harmony's left global sidebar"
);
assertExcludes(
footer,
'<!-- IMPORT partials/sidebar-right.tpl -->',
"Footer should no longer render Harmony's right global sidebar"
);
assertIncludes(
theme,
'@import "./scss/westgate/topbar";',
'theme.scss should import the focused topbar partial'
);
assertExcludes(
theme,
'@import "./scss/westgate/header";',
'theme.scss should not import the removed brand banner stylesheet'
);
assertIncludes(
stylesheet,
'.wg-topbar',
'Topbar styles should be scoped to .wg-topbar'
);
assertIncludes(
stylesheet,
'.wg-topbar__utility-list .visible-closed:not(.hidden)',
'Topbar collapsed utility labels should not override hidden count badges'
);
assertIncludes(
client,
'initWestgateTopbar',
'Client bundle should initialize Westgate topbar behavior'
);
[
'component="sidebar/search"',
'component="notifications"',
'component="sidebar/drafts"',
].forEach((component) => {
assertIncludes(topbar, component, `Topbar should preserve live NodeBB hook ${component}`);
});
[
'<!-- IMPORT partials/sidebar/search.tpl -->',
'<!-- IMPORT partials/sidebar/notifications.tpl -->',
'<!-- IMPORT partials/sidebar/chats.tpl -->',
'<!-- IMPORT partials/sidebar/drafts.tpl -->',
'<!-- IMPORT partials/sidebar/user-menu.tpl -->',
].forEach((templateImport) => {
assertIncludes(topbar, templateImport, `Topbar should reuse Harmony live partial ${templateImport}`);
});
[
'Velessa Thorne',
'Guildmaster Orin',
'A letter left at the Undergate',
'wg-topbar-demo',
"localStorage.setItem('wg-topbar-presence'",
].forEach((fixture) => {
assertExcludes(topbar, fixture, `Topbar should not ship preview fixture ${fixture}`);
});
assertIncludes(topbar, '{{{ if config.loggedIn }}}', 'Topbar should branch on the real logged-in state');
assertIncludes(topbar, '{{{ if canChat }}}', 'Chat controls should remain permission-gated');
assertIncludes(topbar, '{{{ if allowRegistration }}}', 'Registration should remain server-gated');
assertIncludes(topbar, '{{{ each navigation }}}', 'Topbar should render ACP Navigation items');
assertIncludes(topbar, '{./textClass}', 'Topbar should honor ACP Navigation text visibility classes');
assertExcludes(topbar, 'href="{relative_path}/admin"', 'Topbar should not hard-code the administrator link');
assertMatches(
topbar,
/<a\b(?=[^>]*class="[^"]*\bwg-topbar__brand\b[^"]*")(?=[^>]*aria-label="{config\.siteTitle}")/,
'Topbar brand link should use the configured site title as its accessible name'
);
assertMatches(
topbar,
/<span\b(?=[^>]*class="[^"]*\bwg-topbar__brand-name\b[^"]*")[^>]*>{config\.siteTitle}<\/span>/,
'Topbar brand text should render the configured site title'
);
assertExcludes(topbar, 'data-wg-menu', 'Topbar should rely on Bootstrap and NodeBB navigation instead of custom menu triggers');
assertMissing(
'templates/partials/header/brand.tpl',
'Theme should inherit Harmony brand markup instead of shipping the removed Westgate brand banner override'
);
assertMissing(
'scss/westgate/_header.scss',
'Removed brand banner styles should not remain in the theme'
);
assertMissing(
'public/images/plum-header-bg.png',
'Removed brand banner background should not remain in static assets'
);
assertExcludes(
themeHooks,
'brand-header',
'Removed brand banner widget area should not remain registered'
);
const unreadDrawer = read('templates/partials/header/unread-drawer.tpl');
assertIncludes(
topbar,
'<!-- IMPORT partials/header/unread-drawer.tpl -->',
'Topbar should mount the unread drawer before notifications'
);
assert(
topbar.indexOf('partials/header/unread-drawer.tpl') < topbar.indexOf('partials/sidebar/notifications.tpl'),
'Unread drawer should sit before the notifications item'
);
assertIncludes(
unreadDrawer,
'component="unread/count"',
'Unread toggle should carry the live core count badge'
);
assertIncludes(
unreadDrawer,
'data-wg-unread-menu',
'Unread dropdown menu should expose the JS mount hook'
);
assertMatches(
topbar,
/href="\{relative_path\}\/unread"[^>]*>[\s\S]*?component="unread\/count"/,
'Mobile drawer actions should include an Unread link with count badge'
);
assertIncludes(
stylesheet,
'.unread-dropdown',
'Topbar styles should cover the unread dropdown'
);
const supportDrawer = read('templates/partials/header/support-drawer.tpl');
assertIncludes(
topbar,
'<!-- IMPORT partials/header/support-drawer.tpl -->',
'Topbar should mount the support inbox drawer'
);
assertIncludes(
topbar,
'{{{ if support.visible }}}',
'Support inbox drawer should be gated on the viewer having any support access'
);
assert(
topbar.indexOf('partials/sidebar/notifications.tpl') < topbar.indexOf('partials/header/support-drawer.tpl'),
'Support inbox should sit next to (after) the notifications bell'
);
assertIncludes(
supportDrawer,
'component="support/count"',
'Support toggle should carry its own unread count badge'
);
assertIncludes(
supportDrawer,
'component="support/list"',
'Support dropdown menu should expose the JS mount hook'
);
assertIncludes(
supportDrawer,
'notifications-dropdown',
'Support drawer should reuse Harmony\'s notification dropdown styling, not a bespoke class'
);
assertIncludes(
supportDrawer,
'notification-list',
'Support inbox list should reuse Harmony\'s own notification-list styling, not a bespoke class'
);
['data-filter="unread"', 'mark-all-read', 'mark-read'].forEach((marker) => {
assertIncludes(
supportDrawer,
marker,
`Support drawer should offer the same controls as the notification drawer (${marker})`
);
});
assertIncludes(
supportDrawer,
'[[westgate:support-inbox.title]]',
'Support inbox strings should be translation keys, matching every other topbar item'
);
assertMatches(
topbar,
/href="\{relative_path\}\/support"[^>]*>[\s\S]*?component="support\/count"/,
'Mobile drawer actions should include a Support inbox link with count badge'
);
assertIncludes(
stylesheet,
'.wg-topbar .notifications-dropdown',
'Topbar styles should cover the notification-style dropdowns the support inbox reuses'
);