## Summary
A Support inbox drawer on the topbar, next to the notification bell, fed by
the plugin-owned support inbox in sow-nodebb-plugin-support#15.
It is not a lookalike: the rows are rendered with core's own
`partials/notifications_list` partial, from entries the support plugin now
returns in core's notification field names, inside a dropdown that copies
Harmony's `partials/sidebar/notifications.tpl`. So the drawer has the
notification drawer's avatar block, stretched link, muted timeago, per-entry
read dot, All/Unread filter row, and the same two footer buttons — and needs
no CSS of its own, because it reuses the `.notifications-dropdown` classes
this theme already styles.
Behaviour matches the bell too: opening the box does not mark anything read;
an entry is read when you click it or use "Mark all read", and the dot toggles
both ways. Only the API paths differ, because the list is plugin-owned (core's
unread count is one aggregate with no hook to carve one type out of it).
Every string is a translation key.
## Test plan
- [x] `for f in tests/*.test.js; do node "$f"; done` — all pass, including the
updated topbar contract test
- [x] Live against a dev NodeBB as a `support:viewall` holder: badge count 4,
four rows, per-entry read dot drops the badge to 3, Unread filter hides
the read row, "Mark all read" clears every dot and hides the badge,
empty state reads "Nothing new" — no console errors
- [x] Screenshotted side by side with the real notifications dropdown; the two
are structurally identical
Depends on sow-nodebb-plugin-support#15 for the endpoints.
Generated with Claude CodeReviewed-on: #45
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
197 lines
6.0 KiB
JavaScript
197 lines
6.0 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'
|
|
);
|
|
|
|
// Support case notifications live in the notification bell, added by
|
|
// nodebb-plugin-support on core's own hooks. This theme owns nothing about
|
|
// them, which is the point — see that plugin's ADR-0002.
|
|
assert(
|
|
!/support/i.test(topbar),
|
|
'Topbar should carry no support-specific markup'
|
|
);
|
|
|
|
assertIncludes(
|
|
stylesheet,
|
|
'.wg-topbar .notifications-dropdown',
|
|
'Topbar styles should cover the notification dropdown'
|
|
);
|