---
name: testing-merqab-website
description: Run and test the Merqab Integrated Solutions website (Next.js 14 + Tailwind v4 + Supabase) locally end-to-end. Use when verifying UI changes or Supabase DB/connectivity changes.
---

# Testing the Merqab website

Next.js 14 (App Router, TypeScript) + Tailwind CSS v4 (CSS-first `@theme` in
`app/globals.css`) + Supabase (Postgres/Auth/Storage).

## Devin Secrets Needed
- `NEXT_PUBLIC_SUPABASE_URL` — Supabase project URL (Data API).
- `NEXT_PUBLIC_SUPABASE_ANON_KEY` — anon key (public reads).
- `SUPABASE_SERVICE_ROLE_KEY` — service role (server-side, bypasses RLS).
- `SUPABASE_DB_URL` — Postgres connection string URI (incl. DB password). REQUIRED
  for any DDL/migrations; the anon/service-role keys only grant Data API access and
  CANNOT run `CREATE TABLE`. Get it from Supabase Dashboard → Project Settings →
  Database → Connection string → URI (pooler, port 6543).

## Run locally
```bash
npm install
# Next.js reads NEXT_PUBLIC_* + SUPABASE_SERVICE_ROLE_KEY from the env (or .env.local).
# To avoid writing secrets to disk, export them in the shell before `npm run dev`:
npm run dev   # http://localhost:3000
```
Lint/build: `npm run lint`, `npm run build` (both should pass; Google Fonts Inter +
Cairo are fetched at build time, so network access is needed).

## Apply / verify a migration
Migrations live in `supabase/migrations/`. Apply them with a Postgres client using
`SUPABASE_DB_URL` (psql, or a small node `pg` script). Use the simple query protocol
so multi-statement SQL runs in one round trip even on the transaction pooler.

### IMPORTANT pitfall: anon `permission denied for table ...`
Tables created over a direct `postgres` connection do NOT inherit Supabase's default
`anon`/`authenticated` GRANTs, so the Data API returns `permission denied` even when
an RLS `SELECT USING (true)` policy exists. RLS is necessary but not sufficient —
you also need table GRANTs. The migration includes:
```sql
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
GRANT ALL ON ALL TABLES IN SCHEMA public TO anon, authenticated, service_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO anon, authenticated, service_role;
```
RLS still gates actual row access, so broad GRANTs are safe. If you add new tables and
the Data API can't read them, check GRANTs first.

Also note: PostgreSQL requires `WITH CHECK` (not `USING`) for `INSERT` policies and the
write path of `FOR ALL` policies.

## Smoke test (end-to-end)
1. Home page `/` — heading renders as an orange→blue gradient and the
   "Request a Quote" button is orange, turning blue on hover. If Tailwind v4 `@theme`
   is broken these custom classes (`from-primary`, `bg-primary`) won't apply.
2. Supabase connectivity — a server component using `lib/supabase/server.ts` can read
   the `settings` row (e.g. `email: m.hassan@mnsksa.com`). A quick way is a temporary
   route that selects from `settings`; remove it after (do not commit). Note: App Router
   ignores folders prefixed with `_` (private), so name a temp route WITHOUT the underscore
   (e.g. `app/seed-check/page.tsx`) if you want it routable, then delete it.

## Verifying Tailwind v4 classes actually generated
When a change touches `app/globals.css` (e.g. `@source` directives, `@theme`), confirm
brand classes still compile by grepping the SERVED stylesheet from the running dev
server — this is the source of truth (the standalone `@tailwindcss/cli` uses different
automatic content detection and may NOT match what `next dev` / `@tailwindcss/postcss`
emits, so don't rely on the CLI for this check):
```bash
CSS=$(curl -s http://localhost:3000/ | grep -oE '/_next/static/css/[^"]+\.css' | head -1)
curl -s "http://localhost:3000$CSS" > /tmp/devcss.css
# escape the ':' in variant class names when grepping the compiled file:
grep -q 'hover\\:bg-secondary' /tmp/devcss.css && echo PRESENT || echo MISSING
```
Note: `@source not "../.agents"` in `app/globals.css` excludes the agent-skill markdown
from Tailwind scanning (their `bg-[url('...')]` examples otherwise emit broken `url()`
rules and break `next build`). It does NOT drop real classes from `app/**` — verify
with the grep above.

### Recording artifact: `:hover` can't be captured
When recording, the recorder hides/warps the OS cursor, so CSS `:hover` states (e.g. the
blue `hover:bg-secondary` button) won't show in screenshots even though they work. Don't
report this as a failure — verify the hover rule exists in the served CSS instead.

## Verifying seeded catalog content (anon path)
Seed lives in `scripts/seed.mjs` (idempotent: clears+reinserts only services,
sub_services, items, vendors). To verify through the anon key (the path the public site
uses), add a temporary route that does `count: 'exact', head: true` per table, plus a
nested `services -> sub_services -> items` select and a sample item. Expected current
counts: 3 services, 21 sub-services, 88 items, 51 vendors; per-service item split is
8/43 (Telecom), 6/30 (Electrical), 7/15 (Fire). Every row should have non-empty `*_en`
AND `*_ar` (bilingual). Delete the temp route after; do not commit.

## Verifying the public `merqab-media` storage bucket
Upload a tiny test object with the service-role key, then fetch its PUBLIC url with no
auth — expect HTTP 200 + correct content-type:
```bash
# upload via @supabase/supabase-js storage.from('merqab-media').upload(path, bytes, {upsert:true})
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' \
  "$NEXT_PUBLIC_SUPABASE_URL/storage/v1/object/public/merqab-media/<path>"
```
Cleanup: `storage.from('merqab-media').remove([path])`. The public URL may still return
200 briefly after deletion due to CDN caching — confirm deletion with
`storage.from('merqab-media').list('<folder>')` (bypasses the CDN) instead of the URL.
Bucket metadata should be `{ name: 'merqab-media', public: true }` (check via
`storage.getBucket('merqab-media')`).

## Public layout (Navbar / Footer / i18n) testing
The shared layout lives in `components/layout/` and is wired in `app/layout.tsx`
(server reads the `merqab-lang` cookie, sets `<html lang dir>`, and fetches the
`settings` row server-side for the footer).

- **Language toggle:** It's a DIRECT toggle (not a selector) — in English it shows
  "العربية" + globe, in Arabic it shows "English". Clicking it should flip the whole
  layout to RTL (logo moves to the right, nav labels become Arabic, CTA → "اطلب عرضاً")
  and persist across reload via the `merqab-lang` cookie. If it reverts on reload, the
  cookie/SSR wiring is broken.
- **Active link:** Navbar uses `usePathname`; the active link gets a gradient underline.
  Only `/` exists today, so navigate to e.g. `/vendors` and confirm the underline moves
  even though the page itself 404s.
- **Mobile hamburger:** Easiest way to test responsive behavior WITHOUT devtools is to
  resize the actual browser window with wmctrl:
  ```bash
  wmctrl -r :ACTIVE: -b remove,maximized_vert,maximized_horz; sleep 0.5; wmctrl -r :ACTIVE: -e 0,40,20,430,740
  # ...test, then restore:
  wmctrl -r :ACTIVE: -b add,maximized_vert,maximized_horz
  ```
  Below the `md` breakpoint the desktop links/CTA hide and a hamburger appears; opening
  it shows all 8 links + the language toggle + CTA, and it should auto-close on link click.
- **Footer:** headings must be SOLID `#0D1B2A` (no gradient — footer gradient is
  explicitly forbidden). Contact values are LIVE from `settings` (phone/email/address,
  WhatsApp derived from `settings.whatsapp`). Columns fade-in-up on scroll (whileInView, once).

### Expected: 404s on unbuilt pages
Until the page tasks (Home content, About, Services, Vendors, Media Center, Contact)
are built, every nav link except `/` returns Next.js's 404. This is EXPECTED when
testing the layout in isolation — the Navbar/Footer still render on the 404 page
(they're in the root layout) and active-link logic still works. Don't report these
404s as layout bugs.
