5b4a05afb2
Researched mosque-app and community-board best practices (MadinaAPPS,
Nextdoor) before building: the pattern that works is one-tap RSVP with
what/who/where/when up front, plus a clean separation between plain
announcements and dated events. Deliberately did NOT auto-populate a
global Islamic calendar (Eid/Ramadan dates) — those are moon-sighting-
dependent and hardcoding one as fact would be exactly the kind of
fabrication this app avoids everywhere else (real Overpass data, real
Quran API, no invented listings). The Event Board is community-submitted
only: a mosque posting its own Eid prayer time is real information: this
app guessing the date isn't.
Implementation extends the existing Neighbourhood board rather than
building a parallel system: nf_neighbourhood_posts gains is_event/
event_date/event_location, plus a new nf_event_rsvps table (RLS reuses
the existing nf_is_neighbourhood_member helper via a join, no duplicated
logic). UI adds an event toggle on the post form (reveals date/time/
location), a 📅-badged event card with one-tap 'I'm going' RSVP + live
count, and an All/Events/Announcements filter with events sorted
soonest-first.
Seeded a real demo neighbourhood, 'Masjid Al-Falah Kariah' (join code
ALFALAH), with all 5 demo family owners plus a mutawalli as members: 3
announcements and 4 dated, RSVP'd events (Jumu'ah khutbah, weekly Quran
circle, an Islamic finance/estate-planning talk tying back to the app's
own core purpose, and a Ramadan iftar potluck). Caught and fixed two
seeding bugs during verification: timestamps stored without timezone
context displayed several hours off from the intended Malaysia-local
time, and a 'Saturday' event that actually landed on a Sunday — both
fixed by computing against real weekdays/timezone rather than guessing
offsets.
Covered by e2e-event-board.cjs (14/14), including live verification that
the seeded demo neighbourhood renders its real content. Full regression:
all suites pass.
104 lines
6.6 KiB
JavaScript
104 lines
6.6 KiB
JavaScript
// Verifies the Event Board extension to the Neighbourhood board: posting an
|
|
// event (date/time/location), RSVP toggle + count, the Announcements/Events
|
|
// filter, and that the seeded demo neighbourhood ("Masjid Al-Falah Kariah")
|
|
// renders real content for a demo account.
|
|
const { chromium } = require('playwright');
|
|
const { signInFreshFamily, gotoTab } = require('./e2e-auth-helper.cjs');
|
|
const BASE = 'https://moslem04.falahos.my/';
|
|
const results = [];
|
|
const consoleErrors = [];
|
|
function record(name, pass, detail = '') { results.push({ name, pass, detail }); console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${detail ? ' — ' + detail : ''}`); }
|
|
|
|
async function main() {
|
|
const browser = await chromium.launch();
|
|
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
|
|
page.on('pageerror', e => consoleErrors.push(e.message));
|
|
|
|
await signInFreshFamily(page, BASE, 'e2e-eventboard');
|
|
await gotoTab(page, 'Neighbourhood');
|
|
await page.waitForTimeout(600);
|
|
const addToggle = await page.locator('.add-toggle').isVisible().catch(() => false);
|
|
if (addToggle) await page.locator('.add-toggle').click();
|
|
await page.waitForTimeout(300);
|
|
await page.locator('.form-card .field:has-text("Name") input').fill(`Event Board Test ${Date.now()}`);
|
|
await page.locator('button.btn-primary', { hasText: 'Create & get a join code' }).click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Plain announcement — no event fields shown until the toggle is checked
|
|
const eventFieldsHiddenByDefault = !(await page.locator('.field:has-text("Date & time")').isVisible().catch(() => false));
|
|
record('Neighbourhood: event date/location fields hidden until toggled', eventFieldsHiddenByDefault);
|
|
|
|
await page.locator('.field:has-text("Title") input').fill('General announcement');
|
|
await page.locator('button.btn-primary', { hasText: 'Post announcement' }).click();
|
|
await page.waitForTimeout(1000);
|
|
record('Neighbourhood: plain announcement posts without an event badge', await page.locator('.post-row:not(.event-row)', { hasText: 'General announcement' }).isVisible().catch(() => false));
|
|
|
|
// Event post
|
|
await page.locator('.field:has-text("Title") input').fill('Community Quran Class');
|
|
await page.locator('.event-toggle input').check();
|
|
await page.waitForTimeout(200);
|
|
const eventFieldsShown = await page.locator('.field:has-text("Date & time")').isVisible().catch(() => false);
|
|
record('Neighbourhood: event fields appear once toggled', eventFieldsShown);
|
|
await page.locator('.field:has-text("Date & time") input').fill('2026-12-01T18:30');
|
|
await page.locator('.field:has-text("Location") input').fill('Masjid Test Hall');
|
|
await page.locator('button.btn-primary', { hasText: 'Post event' }).click();
|
|
await page.waitForTimeout(1000);
|
|
|
|
const eventCardVisible = await page.locator('.event-row', { hasText: 'Community Quran Class' }).isVisible().catch(() => false);
|
|
record('Event Board: event posts with the 📅 badge and event styling', eventCardVisible);
|
|
const eventDetailsVisible = await page.locator('.event-row', { hasText: 'Community Quran Class' }).locator('.event-when, .event-where').count();
|
|
record('Event Board: shows date/time and location on the event card', eventDetailsVisible === 2, `${eventDetailsVisible} detail lines`);
|
|
|
|
// RSVP
|
|
const rsvpBtn = page.locator('.event-row', { hasText: 'Community Quran Class' }).locator('.rsvp-btn');
|
|
record('Event Board: starts as not going', !(await rsvpBtn.evaluate(el => el.classList.contains('going'))));
|
|
await rsvpBtn.click();
|
|
await page.waitForTimeout(800);
|
|
record('Event Board: RSVP toggles to "Going" with a count of 1', await rsvpBtn.evaluate(el => el.classList.contains('going')) && (await rsvpBtn.textContent()).includes('1'));
|
|
await rsvpBtn.click();
|
|
await page.waitForTimeout(800);
|
|
record('Event Board: RSVP toggles back off (cancel)', !(await rsvpBtn.evaluate(el => el.classList.contains('going'))));
|
|
|
|
// Filter
|
|
await page.locator('.board-filter button', { hasText: 'Events' }).click();
|
|
await page.waitForTimeout(300);
|
|
const onlyEventsShown = await page.locator('.post-row').count() === 1 && await page.locator('.event-row').isVisible();
|
|
record('Event Board: Events filter hides plain announcements', onlyEventsShown);
|
|
await page.locator('.board-filter button', { hasText: 'Announcements' }).click();
|
|
await page.waitForTimeout(300);
|
|
const onlyAnnouncementsShown = await page.locator('.post-row').count() === 1 && !(await page.locator('.event-row').isVisible().catch(() => false));
|
|
record('Event Board: Announcements filter hides events', onlyAnnouncementsShown);
|
|
|
|
// ── Seeded demo neighbourhood ──
|
|
const demoPage = await browser.newPage({ viewport: { width: 390, height: 844 } });
|
|
demoPage.on('console', m => { if (m.type() === 'error') consoleErrors.push('[demo] ' + m.text()); });
|
|
await demoPage.goto(BASE, { waitUntil: 'networkidle' });
|
|
await demoPage.locator('.field:has-text("Email") input').fill('nf.demo.ismail@gmail.com');
|
|
await demoPage.locator('.field:has-text("Password") input').fill('DemoPassword123!');
|
|
await demoPage.locator('button.btn-primary', { hasText: 'Sign in' }).click();
|
|
await demoPage.waitForTimeout(1500);
|
|
if (await demoPage.locator('.switcher-screen').isVisible().catch(() => false)) {
|
|
await demoPage.locator('.family-row').first().click();
|
|
await demoPage.waitForTimeout(1000);
|
|
}
|
|
await gotoTab(demoPage, 'Neighbourhood');
|
|
await demoPage.waitForTimeout(800);
|
|
record('Demo neighbourhood: Masjid Al-Falah Kariah is visible to the demo owner', await demoPage.locator('.active-card', { hasText: 'Masjid Al-Falah Kariah' }).isVisible().catch(() => false));
|
|
const demoPostCount = await demoPage.locator('.post-row').count();
|
|
record('Demo neighbourhood: has real seeded content (7 posts)', demoPostCount === 7, `${demoPostCount} posts`);
|
|
const demoEventCount = await demoPage.locator('.event-row').count();
|
|
record('Demo neighbourhood: has real seeded events (4 events)', demoEventCount === 4, `${demoEventCount} events`);
|
|
await demoPage.close();
|
|
|
|
record('No uncaught JS console errors during full session', consoleErrors.length === 0, consoleErrors.slice(0, 5).join(' || '));
|
|
|
|
await browser.close();
|
|
const passCount = results.filter(r => r.pass).length;
|
|
const failCount = results.length - passCount;
|
|
console.log(`\n${passCount} passed, ${failCount} failed, ${results.length} total`);
|
|
if (failCount > 0) results.filter(r => !r.pass).forEach(r => console.log(` - ${r.name}: ${r.detail}`));
|
|
process.exit(failCount > 0 ? 1 : 0);
|
|
}
|
|
main().catch(e => { console.error('SCRIPT ERROR:', e); process.exit(2); });
|