bud panels duplicate-add guard: server-side already_present flag + client-side error Brief w. FYI flash highlight on the existing entry — for each of the three #id_bud_btn panels (My Buds / post-share / gatekeeper-invite), the JSON response from add_bud / share_post / invite_gamer now carries {already_present, recipient_display, recipient_user_id}; bud-btn.js branches on already_present → calls new Brief.showDuplicateBanner({display_name, target_selector}) instead of the normal onSuccess append; banner title reads @<username> is already present, NVM dismisses, FYI dismisses AND eases in the .bud-duplicate-flash class (color: var(--terUser); text-shadow: 0 0 .5em var(--ninUser); transition: 600ms) onto the existing element (.bud-entry .bud-name / .post-recipient[data-user-id=…] / .gate-slot.filled[data-user-id=…]); gatekeeper "already present" = recipient is either GateSlot.FILLED + gamer OR has TableSeat OR has a pending RoomInvite (highlight target only set when seated — pending invites have no visible slot); .post-recipient chips + .gate-slot.filled cells gain data-user-id so the FYI selector can find them; my_buds.html now loads note.js via the {% block scripts %} pattern (Brief module is required by the duplicate banner path); bonus: latent test_jasmine.py bug fixed — "0 failures" in result.text matched "10 failures" / "20 failures" / etc, silently passing up to 99 failed specs; replaced w. re.search(r"(?<!\d)0 failures\b", …) (caught my new red specs, would've caught any prior Jasmine regression); 18 new ITs + 10 new Jasmine specs + 3 new FTs (one per panel) — TDD
Code architected by Disco DeDisco <discodedisco@outlook.com> Git commit message Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -171,3 +171,17 @@ html:has(#id_kit_bag_dialog[open]) #id_bud_btn {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Duplicate-add highlight ─────────────────────────────────────────────
|
||||
//
|
||||
// Eased-in flash applied by Brief.showDuplicateBanner's FYI button to a
|
||||
// caller-supplied target element — one of .bud-entry .bud-name (My Buds),
|
||||
// .post-recipient (post share), or .gate-slot.filled (gatekeeper invite).
|
||||
// Persists until page refresh; --terUser color + --ninUser text-shadow
|
||||
// per the duplicate-guard spec.
|
||||
|
||||
.bud-duplicate-flash {
|
||||
color: rgba(var(--terUser), 1);
|
||||
text-shadow: 0 0 0.5em rgba(var(--ninUser), 1);
|
||||
transition: color 600ms ease, text-shadow 600ms ease;
|
||||
}
|
||||
|
||||
@@ -136,6 +136,123 @@ describe('Brief.showBanner', () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Brief.showDuplicateBanner — the "already added" error variant. Distinct
|
||||
// from showBanner because: the server doesn't persist a Brief row for these
|
||||
// (transient, client-only), title is rendered as `@<username> is already
|
||||
// present`, there's no date/square/post_url, and the FYI button toggles
|
||||
// `.bud-duplicate-flash` onto a caller-supplied target element instead of
|
||||
// navigating.
|
||||
//
|
||||
// API under test:
|
||||
// Brief.showDuplicateBanner({ display_name, target_selector? })
|
||||
// null / missing display_name → no banner
|
||||
// target_selector matches an element → FYI click adds .bud-duplicate-flash
|
||||
// target_selector missing or matches nothing → FYI just dismisses
|
||||
//
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Brief.showDuplicateBanner', () => {
|
||||
|
||||
let fixture;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = document.createElement('div');
|
||||
fixture.id = 'dup-fixture';
|
||||
fixture.innerHTML =
|
||||
'<h2>Dash</h2>' +
|
||||
'<ul id="bud-fixture-list">' +
|
||||
'<li class="bud-entry" data-bud-id="42">' +
|
||||
'<span class="bud-name">alice</span>' +
|
||||
'</li>' +
|
||||
'</ul>';
|
||||
document.body.appendChild(fixture);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.querySelectorAll('.note-banner').forEach(b => b.remove());
|
||||
fixture.remove();
|
||||
});
|
||||
|
||||
it('D1: missing display_name → no banner', () => {
|
||||
Brief.showDuplicateBanner({});
|
||||
expect(document.querySelector('.note-banner')).toBeNull();
|
||||
});
|
||||
|
||||
it('D2: title reads "@<display_name> is already present"', () => {
|
||||
Brief.showDuplicateBanner({ display_name: 'alice' });
|
||||
const title = document.querySelector('.note-banner__title');
|
||||
expect(title).not.toBeNull();
|
||||
expect(title.textContent).toBe('@alice is already present');
|
||||
});
|
||||
|
||||
it('D3: banner carries the .note-banner--duplicate modifier class', () => {
|
||||
Brief.showDuplicateBanner({ display_name: 'alice' });
|
||||
const b = document.querySelector('.note-banner');
|
||||
expect(b).not.toBeNull();
|
||||
expect(b.classList.contains('note-banner--duplicate')).toBeTrue();
|
||||
});
|
||||
|
||||
it('D4: duplicate banner has no .note-banner__description or .note-banner__timestamp', () => {
|
||||
Brief.showDuplicateBanner({ display_name: 'alice' });
|
||||
expect(document.querySelector('.note-banner__description')).toBeNull();
|
||||
expect(document.querySelector('.note-banner__timestamp')).toBeNull();
|
||||
});
|
||||
|
||||
it('D5: FYI button is a <button>, not an <a> (no navigation)', () => {
|
||||
Brief.showDuplicateBanner({ display_name: 'alice' });
|
||||
const fyi = document.querySelector('.note-banner .note-banner__fyi');
|
||||
expect(fyi).not.toBeNull();
|
||||
expect(fyi.tagName.toLowerCase()).toBe('button');
|
||||
});
|
||||
|
||||
it('D6: NVM dismisses without adding .bud-duplicate-flash anywhere', () => {
|
||||
Brief.showDuplicateBanner({
|
||||
display_name: 'alice',
|
||||
target_selector: '.bud-entry[data-bud-id="42"] .bud-name',
|
||||
});
|
||||
document.querySelector('.note-banner__nvm').click();
|
||||
expect(document.querySelector('.note-banner')).toBeNull();
|
||||
const name = fixture.querySelector('.bud-name');
|
||||
expect(name.classList.contains('bud-duplicate-flash')).toBeFalse();
|
||||
});
|
||||
|
||||
it('D7: FYI dismisses AND adds .bud-duplicate-flash to the target element', () => {
|
||||
Brief.showDuplicateBanner({
|
||||
display_name: 'alice',
|
||||
target_selector: '.bud-entry[data-bud-id="42"] .bud-name',
|
||||
});
|
||||
document.querySelector('.note-banner__fyi').click();
|
||||
expect(document.querySelector('.note-banner')).toBeNull();
|
||||
const name = fixture.querySelector('.bud-name');
|
||||
expect(name.classList.contains('bud-duplicate-flash')).toBeTrue();
|
||||
});
|
||||
|
||||
it('D8: FYI dismisses cleanly when target_selector is missing', () => {
|
||||
Brief.showDuplicateBanner({ display_name: 'alice' });
|
||||
document.querySelector('.note-banner__fyi').click();
|
||||
expect(document.querySelector('.note-banner')).toBeNull();
|
||||
});
|
||||
|
||||
it('D9: FYI dismisses cleanly when target_selector matches nothing', () => {
|
||||
Brief.showDuplicateBanner({
|
||||
display_name: 'alice',
|
||||
target_selector: '.gate-slot[data-user-id="does-not-exist"]',
|
||||
});
|
||||
document.querySelector('.note-banner__fyi').click();
|
||||
expect(document.querySelector('.note-banner')).toBeNull();
|
||||
});
|
||||
|
||||
it('D10: title escapes HTML in display_name', () => {
|
||||
Brief.showDuplicateBanner({ display_name: '<script>x</script>' });
|
||||
const title = document.querySelector('.note-banner__title');
|
||||
expect(title.textContent).toBe('@<script>x</script> is already present');
|
||||
expect(title.querySelector('script')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Brief.handleSaveResponse', () => {
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user