Files
pn-new-crm/tests/e2e/smoke/31-i18n-form-fields.spec.ts
Matt Ciaccio 16d98d630e feat(i18n): country/phone/timezone/subdivision primitives + form wiring
Cross-cutting i18n polish for forms across the marina + residential + company
domains. Introduces a single source of truth for country/phone/timezone/
subdivision data and replaces every nationality-as-free-text and timezone-
as-string Input with a dedicated combobox.

PR1  Countries — ALL_COUNTRY_CODES (~250 ISO-3166-1 alpha-2), Intl.DisplayNames
     for localized labels, detectDefaultCountry() with navigator-region
     fallback to US, CountryCombobox with regional-indicator flag glyphs +
     compact mode for inline use.
PR2  Phone — libphonenumber-js wrapper (parsePhone / formatAsYouType /
     callingCodeFor), PhoneInput with flag dropdown + national-format
     AsYouType + paste-detect that flips the country dropdown for pasted
     international strings.
PR3  Timezones — country->IANA map (250 entries, multi-zone for AU/BR/CA/CD/
     ID/KZ/MN/MX/RU/US), formatTimezoneLabel ("Europe/London (UTC+1)"),
     TimezoneCombobox with Suggested/All grouping driven by countryHint.
PR4  Subdivisions — wraps the iso-3166-2 npm package (~5000 ISO 3166-2
     codes for every country), per-country cache, SubdivisionCombobox with
     "Pick a country first" / "No regions available" empty states.
PR5  Schema deltas (migration 0015) — clients.nationality_iso, clientContacts
     {value_e164, value_country}, clientAddresses {country_iso, subdivision_iso},
     residentialClients {phone_e164, phone_country, nationality_iso, timezone,
     place_of_residence_country_iso, subdivision_iso}, companies {incorporation_
     country_iso, incorporation_subdivision_iso}, companyAddresses {country_iso,
     subdivision_iso}. Plus shared zod validators (validators/i18n.ts) used
     by every entity validator + route handler.
PR6  ClientForm + ClientDetail — CountryCombobox replaces nationality Input,
     TimezoneCombobox replaces timezone Input (driven by nationalityIso hint),
     PhoneInput conditionally rendered for phone/whatsapp contacts. Inline
     editors (InlineCountryField / InlineTimezoneField / InlinePhoneField)
     for the detail-page overview rows + ContactsEditor.
PR7  Residential client form + detail — phone -> PhoneInput, nationality/
     timezone/place-of-residence-country/subdivision rows in both create
     sheet and inline-editable detail view. Subdivision wipes when country
     flips since codes are country-scoped.
PR8  Company form + detail — incorporation country -> CountryCombobox,
     incorporation region -> SubdivisionCombobox in both modes.
PR9  Public inquiry endpoint — accepts pre-normalized phoneE164/phoneCountry
     and i18n fields from newer website builds, server-side parsePhone()
     fallback for legacy raw-international submissions. Old Nuxt builds
     keep working unchanged.

Tests: 4 unit suites for the primitives (25 tests), 1 integration spec for
the public phone-normalization path (3 tests), 1 smoke spec asserting the
combobox triggers render in all three create sheets.

Test totals: vitest 713 -> 741 (+28).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 18:13:08 +02:00

66 lines
2.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* i18n PR11 — combobox surfaces.
*
* Proves the new country / timezone / phone / subdivision combobox triggers
* actually render in the create sheets we wired in PR68. Doesn't exercise
* the full data round-trip (covered by integration tests + form-spec
* coverage); this spec just guards the wiring against regression.
*/
import { test, expect } from '@playwright/test';
import { login, navigateTo, waitForSheet } from './helpers';
test.describe('i18n combobox wiring', () => {
test.beforeEach(async ({ page }) => {
await login(page, 'super_admin');
});
test('new residential client form exposes phone, country, timezone, subdivision pickers', async ({
page,
}) => {
await navigateTo(page, '/residential/clients');
await page.locator('main').getByRole('button', { name: /^new$/i }).first().click();
await waitForSheet(page);
const sheet = page.locator('[role="dialog"]');
// PhoneInput renders a flag dropdown + national-format input.
await expect(sheet.locator('[data-testid="rc-phone-country"]')).toBeVisible();
// Country / timezone combobox triggers.
await expect(sheet.locator('[data-testid="rc-nationality"]')).toBeVisible();
await expect(sheet.locator('[data-testid="rc-timezone"]')).toBeVisible();
// Country of residence + subdivision (subdivision is disabled until country picked).
await expect(sheet.locator('[data-testid="rc-residence-country"]')).toBeVisible();
await expect(sheet.locator('[data-testid="rc-residence-subdivision"]')).toBeVisible();
});
test('new client form swaps nationality input for CountryCombobox', async ({ page }) => {
await navigateTo(page, '/clients');
// Sheet trigger label varies by tenant — stick to the topbar action.
await page
.locator('main')
.getByRole('button', { name: /^new client$/i })
.first()
.click();
await waitForSheet(page);
const sheet = page.locator('[role="dialog"]');
await expect(sheet.locator('[data-testid="client-nationality"]')).toBeVisible();
await expect(sheet.locator('[data-testid="client-timezone"]')).toBeVisible();
});
test('new company form exposes incorporation country + subdivision pickers', async ({ page }) => {
await navigateTo(page, '/companies');
await page
.locator('main')
.getByRole('button', { name: /^new company$/i })
.first()
.click();
await waitForSheet(page);
const sheet = page.locator('[role="dialog"]');
await expect(sheet.locator('[data-testid="company-incorp-country"]')).toBeVisible();
await expect(sheet.locator('[data-testid="company-incorp-subdivision"]')).toBeVisible();
});
});