docs(leads): document Leads store migration and payment UI fix; note tests update
This commit is contained in:
243
tests/leads_add_lead.test.ts
Normal file
243
tests/leads_add_lead.test.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user