244 lines
11 KiB
TypeScript
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();
|
|
});
|
|
});
|