Files
OSIT-AE-App-Svelte/tests/leads_add_lead.test.ts

244 lines
11 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { testing_event_id } from './_helpers/env';
import {
testing_exhibit_id,
exhibit_staff_passcode,
setup_leads_test_page,
minimal_badge,
} from './_helpers/leads_helpers';
const event_id = testing_event_id;
const exhibit_id = testing_exhibit_id;
const exhibit_url = `/events/${event_id}/leads/exhibit/${exhibit_id}`;
// Pre-seeded auth used by all tests in this suite
const signed_in_kv = { [exhibit_id]: { key: exhibit_staff_passcode, type: 'shared' } };
// Seed leads_overrides to start in search mode (not QR).
// Without this, the add tab shows the QR scanner which cannot be exercised in Playwright.
const search_mode_seed = { tab_add_mode: { [exhibit_id]: 'search' } };
test.describe('Leads — Add Lead (manual search)', () => {
// -----------------------------------------------------------------------
// 1. Navigate to Add tab and see search form
// -----------------------------------------------------------------------
test('clicking Add Lead shows manual search form', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
auth_kv: signed_in_kv,
leads_overrides: search_mode_seed,
});
await page.goto(exhibit_url);
// Click "Add Lead" header button to enter the add tab
await page.locator('header button.preset-filled-primary').click();
// Manual search form must be visible
await expect(
page.locator('input[placeholder="Attendee name, email, or badge ID..."]')
).toBeVisible({ timeout: 8_000 });
});
// -----------------------------------------------------------------------
// 2. Search returns results
// -----------------------------------------------------------------------
test('badge search returns results and shows Add button for opted-in attendees', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
auth_kv: signed_in_kv,
leads_overrides: search_mode_seed,
// badge_li defaults to [minimal_badge()] — one badge with allow_tracking=true
});
await page.goto(exhibit_url);
await page.locator('header button.preset-filled-primary').click();
const search_input = page.locator(
'input[placeholder="Attendee name, email, or badge ID..."]'
);
await expect(search_input).toBeVisible({ timeout: 8_000 });
// Type a query and submit the search form
await search_input.fill('Scott');
await page.locator('button:has-text("Search")').click();
// Result card with the badge full_name should appear
await expect(page.locator('text=Scott Idem')).toBeVisible({ timeout: 8_000 });
// The "Add" button must be visible (allow_tracking=true on the mock badge)
await expect(
page.locator('.results-list button.preset-filled-success')
).toBeVisible({ timeout: 5_000 });
});
// -----------------------------------------------------------------------
// 3. Opted-out attendee shows "Opt-Out" badge instead of Add button
// -----------------------------------------------------------------------
test('opted-out attendee shows Opt-Out label instead of Add button', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
auth_kv: signed_in_kv,
leads_overrides: search_mode_seed,
// Override badge_li with one badge that has allow_tracking=false
badge_li: [minimal_badge({ allow_tracking: false, full_name: 'Jane Opted Out' })],
});
await page.goto(exhibit_url);
await page.locator('header button.preset-filled-primary').click();
const search_input = page.locator(
'input[placeholder="Attendee name, email, or badge ID..."]'
);
await expect(search_input).toBeVisible({ timeout: 8_000 });
await search_input.fill('Jane');
await page.locator('button:has-text("Search")').click();
await expect(page.locator('text=Jane Opted Out')).toBeVisible({ timeout: 8_000 });
// Add button must NOT appear
await expect(
page.locator('.results-list button.preset-filled-success')
).not.toBeVisible();
// "Opt-Out" label must appear instead
await expect(page.locator('text=Opt-Out')).toBeVisible({ timeout: 5_000 });
});
// -----------------------------------------------------------------------
// 4. Clicking Add creates a lead and shows View link
//
// After a successful create_ae_obj__exhibit_tracking(), the search result
// row switches from "Add" button to a "View" link pointing to the lead
// detail page. This is the primary success path.
// -----------------------------------------------------------------------
test('clicking Add button creates lead and shows View link', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
auth_kv: signed_in_kv,
leads_overrides: search_mode_seed,
});
await page.goto(exhibit_url);
await page.locator('header button.preset-filled-primary').click();
const search_input = page.locator(
'input[placeholder="Attendee name, email, or badge ID..."]'
);
await expect(search_input).toBeVisible({ timeout: 8_000 });
await search_input.fill('Scott');
await page.locator('button:has-text("Search")').click();
// Wait for results
await expect(page.locator('text=Scott Idem')).toBeVisible({ timeout: 8_000 });
// Intercept the tracking create request before clicking Add
const create_promise = page.waitForRequest(
(r) =>
r.url().includes('event_exhibit_tracking') &&
r.method() === 'POST',
{ timeout: 5_000 }
);
await page.locator('.results-list button.preset-filled-success').click();
// The POST must have been made
const create_req = await create_promise;
const body = JSON.parse(create_req.postData() ?? '{}');
expect(body.event_badge_id).toBe('UIJT-73-63-61');
// Shared-passcode users store 'shared_passcode' as their identity
expect(body.external_person_id).toBe('shared_passcode');
// "Add" button should disappear; "View" link should appear
await expect(
page.locator('.results-list button.preset-filled-success')
).not.toBeVisible({ timeout: 5_000 });
await expect(
page.locator('.results-list a.preset-filled-secondary')
).toBeVisible({ timeout: 5_000 });
// View link must point to the tracking detail page
const view_href = await page
.locator('.results-list a.preset-filled-secondary')
.getAttribute('href');
expect(view_href).toContain(`/leads/exhibit/${exhibit_id}/lead/`);
});
// -----------------------------------------------------------------------
// 5. Search with no results shows empty-state message
// -----------------------------------------------------------------------
test('search with no results shows empty-state message', async ({ page }) => {
await setup_leads_test_page(page, event_id, exhibit_id, {
auth_kv: signed_in_kv,
leads_overrides: search_mode_seed,
badge_li: [], // empty results
});
await page.goto(exhibit_url);
await page.locator('header button.preset-filled-primary').click();
const search_input = page.locator(
'input[placeholder="Attendee name, email, or badge ID..."]'
);
await expect(search_input).toBeVisible({ timeout: 8_000 });
await search_input.fill('nobody');
await page.locator('button:has-text("Search")').click();
await expect(
page.locator('text=No attendees found matching')
).toBeVisible({ timeout: 5_000 });
});
// -----------------------------------------------------------------------
// 6. "My Leads" filter resolves correctly for shared-passcode users
//
// When tracking__qry__licensee_email = 'my' and the user is authenticated
// via shared passcode, the filter must resolve to 'shared_passcode'
// (not the literal passcode string, which would never match any record).
// This is the bug fixed in 2026-04-01 — a regression guard.
// -----------------------------------------------------------------------
test('My Leads filter resolves to shared_passcode for shared-auth users', async ({ page }) => {
const tracking_record = {
id: 'TRK-MY-001',
event_exhibit_tracking_id: 'TRK-MY-001',
event_exhibit_id: exhibit_id,
event_badge_id: 'UIJT-73-63-61',
event_badge_full_name: 'Scott Idem',
event_badge_email: 'scott@demo.oneskyit.com',
// Stored identity for shared-passcode captures must be the literal
// 'shared_passcode', not the actual passcode value.
external_person_id: 'shared_passcode',
group: 'shared_passcode',
enable: 1,
hide: false,
created_on: new Date().toISOString(),
updated_on: new Date().toISOString(),
};
await setup_leads_test_page(page, event_id, exhibit_id, {
auth_kv: signed_in_kv,
tracking_li: [tracking_record],
leads_overrides: {
// Pre-set the filter to "My Leads"
tracking__qry__licensee_email: 'my',
},
});
await page.goto(exhibit_url);
// The list tab should show; with the "My Leads" filter active and the
// tracking record's external_person_id = 'shared_passcode', it must pass
// through the HARD GUARD in filtered_lead_li.
//
// We cannot directly assert the filter resolved correctly without reading
// the store, but we can assert the lead card IS visible (meaning the filter
// did not incorrectly drop it). If the filter resolved to the raw passcode
// string ('BOOTH2026'), the record would be excluded and the list empty.
await expect(page.locator('text=Lead List')).toBeVisible({ timeout: 8_000 });
// The search will run against Dexie (which is empty — IDB not pre-seeded here).
// Asserting no crash and correct page structure is the smoke-level check.
// Full IDB-backed "My Leads" verification belongs in an IDB inject-then-reload test.
await expect(page.locator('.ae_events_leads_tracking_new')).toBeVisible();
});
});