diff --git a/lib/PortingEmbed/EmbedField.tsx b/lib/PortingEmbed/EmbedField.tsx
new file mode 100644
index 0000000..033855c
--- /dev/null
+++ b/lib/PortingEmbed/EmbedField.tsx
@@ -0,0 +1,12 @@
+type Props = {
+ children: React.ReactNode
+}
+
+export function EmbedField({ children }: Props) {
+ // TODO: customizable classNames
+ return (
+
+ {children}
+
+ )
+}
diff --git a/lib/PortingEmbed/EmbedFieldError.tsx b/lib/PortingEmbed/EmbedFieldError.tsx
new file mode 100644
index 0000000..41e564b
--- /dev/null
+++ b/lib/PortingEmbed/EmbedFieldError.tsx
@@ -0,0 +1,16 @@
+type Props = {
+ error: string
+}
+
+export function EmbedFieldError({ error }: Props) {
+ if (!error) {
+ return null
+ }
+
+ // TODO: customizable classNames
+ return (
+
+ {error}
+
+ )
+}
diff --git a/lib/PortingEmbed/EmbedFieldInput.tsx b/lib/PortingEmbed/EmbedFieldInput.tsx
new file mode 100644
index 0000000..f72021e
--- /dev/null
+++ b/lib/PortingEmbed/EmbedFieldInput.tsx
@@ -0,0 +1,11 @@
+type Props = React.HTMLAttributes
+
+export function EmbedFieldInput(props: Props) {
+ // TODO: customizable classNames
+ return (
+
+ )
+}
diff --git a/lib/PortingEmbed/EmbedFieldLabel.tsx b/lib/PortingEmbed/EmbedFieldLabel.tsx
new file mode 100644
index 0000000..c6a78b2
--- /dev/null
+++ b/lib/PortingEmbed/EmbedFieldLabel.tsx
@@ -0,0 +1,11 @@
+type Props = React.HTMLAttributes
+
+export function EmbedFieldLabel(props: Props) {
+ // TODO: customizable classNames
+ return (
+
+ )
+}
diff --git a/lib/PortingEmbed/PortingEmbed.tsx b/lib/PortingEmbed/PortingEmbed.tsx
index cd44c14..e84a512 100644
--- a/lib/PortingEmbed/PortingEmbed.tsx
+++ b/lib/PortingEmbed/PortingEmbed.tsx
@@ -26,7 +26,7 @@ export function PortingEmbed({
onValidationChange,
}: PortingEmbedProps) {
return (
-
+
unknown
- onSubmit: (data: Partial) => unknown
+ onSubmit: (data: Partial) => unknown
}
-type PortingForm = {
- accountPin: string
- accountNumber: string
- birthday: string
- firstName: string
- lastName: string
-}
+type PortingFormData =
+ | StepCarrierDetailsFormData
+ | StepHolderDetailsFormData
+ | { address?: StepAddressFormData }
+ | StepDonorProviderApprovalFormData
export function PortingForm({ porting, onValidationChange, onSubmit }: Props) {
- const [portingForm, { Form, Field }] = useForm({
- initialValues: {
- accountNumber: porting.accountNumber || '',
- accountPin: '',
- birthday: porting.birthday || '',
- firstName: porting.firstName || '',
- lastName: porting.lastName || '',
- },
- validateOn: 'blur',
- })
-
- useSignalEffect(() => {
- const isValid = !portingForm.invalid.value
- onValidationChange?.({ isValid })
- })
+ const step = wizardStep(porting)
- return (
-
- )
+ return null
}
diff --git a/lib/PortingEmbed/StepAddressForm.tsx b/lib/PortingEmbed/StepAddressForm.tsx
new file mode 100644
index 0000000..88ea042
--- /dev/null
+++ b/lib/PortingEmbed/StepAddressForm.tsx
@@ -0,0 +1,192 @@
+import {
+ pattern,
+ required,
+ toTrimmed,
+ toUpperCase,
+ useForm,
+} from '@modular-forms/preact'
+import { useSignalEffect } from '@preact/signals'
+
+import { Porting } from '../types'
+import { EmbedField } from './EmbedField'
+import { EmbedFieldError } from './EmbedFieldError'
+import { EmbedFieldInput } from './EmbedFieldInput'
+import { EmbedFieldLabel } from './EmbedFieldLabel'
+
+export type StepAddressFormData = {
+ line1: string
+ line2: string | null
+ city: string
+ postalCode: string
+ state: string | null
+ country: string
+}
+
+type Props = {
+ porting: Porting
+ onValidationChange?: (event: { isValid: boolean }) => unknown
+ onSubmit: (data: StepAddressFormData) => unknown
+}
+
+export function StepAddressForm({
+ porting,
+ onValidationChange,
+ onSubmit,
+}: Props) {
+ const [portingForm, { Form, Field }] = useForm({
+ initialValues: {
+ line1: porting.address?.line1 ?? '',
+ line2: porting.address?.line2 ?? null,
+ city: porting.address?.city ?? '',
+ postalCode: porting.address?.postalCode ?? '',
+ state: porting.address?.state,
+ country: porting.address?.country ?? '',
+ },
+ validateOn: 'blur',
+ })
+
+ useSignalEffect(() => {
+ const isValid = !portingForm.invalid.value
+ onValidationChange?.({ isValid })
+ })
+
+ return (
+
+ )
+}
diff --git a/lib/PortingEmbed/StepCarrierDetailsForm.tsx b/lib/PortingEmbed/StepCarrierDetailsForm.tsx
new file mode 100644
index 0000000..bc4b72b
--- /dev/null
+++ b/lib/PortingEmbed/StepCarrierDetailsForm.tsx
@@ -0,0 +1,126 @@
+import { required, setError, toTrimmed, useForm } from '@modular-forms/preact'
+import { useSignalEffect } from '@preact/signals'
+
+import { Porting } from '../types'
+import { EmbedField } from './EmbedField'
+import { EmbedFieldError } from './EmbedFieldError'
+import { EmbedFieldInput } from './EmbedFieldInput'
+import { EmbedFieldLabel } from './EmbedFieldLabel'
+import { sanitizeSubmitData } from './sanitizeSubmitData'
+
+export type StepCarrierDetailsFormData = {
+ accountNumber?: string
+ accountPin?: string
+}
+
+type Props = {
+ porting: Porting
+ onValidationChange?: (event: { isValid: boolean }) => unknown
+ onSubmit: (data: Partial) => unknown
+}
+
+export function StepCarrierDetailsForm({
+ porting,
+ onValidationChange,
+ onSubmit,
+}: Props) {
+ const [form, { Form, Field }] = useForm({
+ initialValues: {
+ accountNumber: porting.accountNumber ?? '',
+ },
+ validateOn: 'blur',
+ })
+
+ useSignalEffect(() => {
+ const isValid = !form.invalid.value
+ onValidationChange?.({ isValid })
+ })
+
+ return (
+
+ )
+}
diff --git a/lib/PortingEmbed/StepDonorProviderApprovalForm.tsx b/lib/PortingEmbed/StepDonorProviderApprovalForm.tsx
new file mode 100644
index 0000000..3c10f92
--- /dev/null
+++ b/lib/PortingEmbed/StepDonorProviderApprovalForm.tsx
@@ -0,0 +1,74 @@
+import { required, useForm } from '@modular-forms/preact'
+import { useSignalEffect } from '@preact/signals'
+
+import { Porting } from '../types'
+import { EmbedField } from './EmbedField'
+import { EmbedFieldError } from './EmbedFieldError'
+import { EmbedFieldInput } from './EmbedFieldInput'
+import { EmbedFieldLabel } from './EmbedFieldLabel'
+import { sanitizeSubmitData } from './sanitizeSubmitData'
+
+export type StepDonorProviderApprovalFormData = {
+ donorProviderApproval?: boolean
+}
+
+type Props = {
+ porting: Porting
+ onValidationChange?: (event: { isValid: boolean }) => unknown
+ onSubmit: (data: Partial) => unknown
+}
+
+export function StepDonorProviderApprovalForm({
+ porting,
+ onValidationChange,
+ onSubmit,
+}: Props) {
+ const [portingForm, { Form, Field }] =
+ useForm({
+ initialValues: {
+ donorProviderApproval: porting.donorProviderApproval ?? false,
+ },
+ validateOn: 'change',
+ })
+
+ useSignalEffect(() => {
+ const isValid = !portingForm.invalid.value
+ onValidationChange?.({ isValid })
+ })
+
+ return (
+
+ )
+}
diff --git a/lib/PortingEmbed/StepHolderDetailsForm.tsx b/lib/PortingEmbed/StepHolderDetailsForm.tsx
new file mode 100644
index 0000000..33a23c9
--- /dev/null
+++ b/lib/PortingEmbed/StepHolderDetailsForm.tsx
@@ -0,0 +1,117 @@
+import { required, toTrimmed, useForm } from '@modular-forms/preact'
+import { useSignalEffect } from '@preact/signals'
+
+import { Porting } from '../types'
+import { EmbedField } from './EmbedField'
+import { EmbedFieldError } from './EmbedFieldError'
+import { EmbedFieldInput } from './EmbedFieldInput'
+import { EmbedFieldLabel } from './EmbedFieldLabel'
+import { sanitizeSubmitData } from './sanitizeSubmitData'
+
+export type StepHolderDetailsFormData = {
+ firstName?: string
+ lastName?: string
+ birthday?: string
+}
+
+type Props = {
+ porting: Porting
+ onValidationChange?: (event: { isValid: boolean }) => unknown
+ onSubmit: (data: Partial) => unknown
+}
+
+export function StepHolderDetailsForm({
+ porting,
+ onValidationChange,
+ onSubmit,
+}: Props) {
+ const [portingForm, { Form, Field }] = useForm({
+ initialValues: {
+ firstName: porting.firstName ?? '',
+ lastName: porting.lastName ?? '',
+ birthday: porting.birthday ?? '',
+ },
+ validateOn: 'blur',
+ })
+
+ useSignalEffect(() => {
+ const isValid = !portingForm.invalid.value
+ onValidationChange?.({ isValid })
+ })
+
+ return (
+
+ )
+}
diff --git a/lib/PortingEmbed/__stories__/PortingEmbed.stories.tsx b/lib/PortingEmbed/__stories__/PortingEmbed.stories.tsx
index 6302571..44a55e8 100644
--- a/lib/PortingEmbed/__stories__/PortingEmbed.stories.tsx
+++ b/lib/PortingEmbed/__stories__/PortingEmbed.stories.tsx
@@ -9,8 +9,7 @@ const meta: Meta = {
component: PortingEmbed,
tags: ['autodocs'],
argTypes: {
- token: { control: 'text' },
- initialPorting: { control: 'object' },
+ porting: { control: 'object' },
onPortingUpdate: { action: 'onPortingUpdate' },
onValidationChange: { action: 'onValidationChange' },
},
@@ -30,19 +29,82 @@ export default meta
export const EmptyPorting = {
args: {
- porting: portingFactory.build(),
+ porting: portingFactory.build({
+ required: [
+ 'accountNumber',
+ 'accountPin',
+ 'address',
+ 'firstName',
+ 'lastName',
+ 'birthday',
+ 'donorProviderApproval',
+ ],
+ }),
},
}
-export const PrefilledPorting = {
+export const MissingHolderPorting = {
args: {
porting: portingFactory
.params({
+ required: [
+ 'accountNumber',
+ 'accountPin',
+ 'address',
+ 'firstName',
+ 'lastName',
+ 'birthday',
+ 'donorProviderApproval',
+ ],
accountNumber: '1234',
accountPinExists: true,
- birthday: '01.01.1990',
- firstName: 'Jane',
- lastName: 'Doe',
+ })
+ .build(),
+ },
+}
+
+export const MissingAddressPorting = {
+ args: {
+ porting: portingFactory
+ .params({
+ required: [
+ 'accountNumber',
+ 'accountPin',
+ 'address',
+ 'firstName',
+ 'lastName',
+ 'birthday',
+ 'donorProviderApproval',
+ ],
+ accountNumber: '1234',
+ accountPinExists: true,
+ firstName: 'first',
+ lastName: 'last',
+ birthday: '1954-04-29',
+ })
+ .build(),
+ },
+}
+
+export const MissingDonorProviderApprovalPorting = {
+ args: {
+ porting: portingFactory
+ .params({
+ required: [
+ 'accountNumber',
+ 'accountPin',
+ 'address',
+ 'firstName',
+ 'lastName',
+ 'birthday',
+ 'donorProviderApproval',
+ ],
+ accountNumber: '1234',
+ accountPinExists: true,
+ firstName: 'first',
+ lastName: 'last',
+ birthday: '1954-04-29',
+ address: {},
})
.build(),
},
diff --git a/lib/PortingEmbed/__stories__/StepAddressForm.stories.tsx b/lib/PortingEmbed/__stories__/StepAddressForm.stories.tsx
new file mode 100644
index 0000000..3c98762
--- /dev/null
+++ b/lib/PortingEmbed/__stories__/StepAddressForm.stories.tsx
@@ -0,0 +1,52 @@
+import { Meta } from '@storybook/preact'
+
+import { portingFactory } from '@/testing/factories/porting'
+
+import { StepAddressForm } from '../StepAddressForm'
+
+const meta: Meta = {
+ title: 'Porting/StepAddressForm',
+ component: StepAddressForm,
+ tags: ['autodocs'],
+ argTypes: {
+ porting: { control: 'object' },
+ onValidationChange: { action: 'onValidationChange' },
+ onSubmit: { action: 'onSubmit' },
+ },
+ decorators: [
+ (Story) => (
+
+ {Story()}
+
+
+ ),
+ ],
+}
+
+export default meta
+
+export const Empty = {
+ args: {
+ porting: portingFactory.build({
+ required: ['address'],
+ }),
+ },
+}
+
+export const Prefilled = {
+ args: {
+ porting: portingFactory.build({
+ required: ['address'],
+ address: {
+ line1: 'line1',
+ line2: null,
+ city: 'city',
+ postalCode: 'pc123',
+ state: 'ST',
+ country: 'CO',
+ },
+ }),
+ },
+}
diff --git a/lib/PortingEmbed/__stories__/StepCarrierDetails.stories.tsx b/lib/PortingEmbed/__stories__/StepCarrierDetails.stories.tsx
new file mode 100644
index 0000000..c8db646
--- /dev/null
+++ b/lib/PortingEmbed/__stories__/StepCarrierDetails.stories.tsx
@@ -0,0 +1,46 @@
+import { Meta } from '@storybook/preact'
+
+import { portingFactory } from '@/testing/factories/porting'
+
+import { StepCarrierDetailsForm } from '../StepCarrierDetailsForm'
+
+const meta: Meta = {
+ title: 'Porting/StepCarrierDetailsForm',
+ component: StepCarrierDetailsForm,
+ tags: ['autodocs'],
+ argTypes: {
+ porting: { control: 'object' },
+ onValidationChange: { action: 'onValidationChange' },
+ onSubmit: { action: 'onSubmit' },
+ },
+ decorators: [
+ (Story) => (
+
+ {Story()}
+
+
+ ),
+ ],
+}
+
+export default meta
+
+export const Empty = {
+ args: {
+ porting: portingFactory.build({
+ required: ['accountNumber', 'accountPin'],
+ }),
+ },
+}
+
+export const Prefilled = {
+ args: {
+ porting: portingFactory.build({
+ required: ['accountNumber', 'accountPin'],
+ accountNumber: '1337',
+ accountPinExists: true,
+ }),
+ },
+}
diff --git a/lib/PortingEmbed/__stories__/StepDonorProviderApproval.stories.tsx b/lib/PortingEmbed/__stories__/StepDonorProviderApproval.stories.tsx
new file mode 100644
index 0000000..417e755
--- /dev/null
+++ b/lib/PortingEmbed/__stories__/StepDonorProviderApproval.stories.tsx
@@ -0,0 +1,45 @@
+import { Meta } from '@storybook/preact'
+
+import { portingFactory } from '@/testing/factories/porting'
+
+import { StepDonorProviderApprovalForm } from '../StepDonorProviderApprovalForm'
+
+const meta: Meta = {
+ title: 'Porting/StepDonorProviderApprovalForm',
+ component: StepDonorProviderApprovalForm,
+ tags: ['autodocs'],
+ argTypes: {
+ porting: { control: 'object' },
+ onValidationChange: { action: 'onValidationChange' },
+ onSubmit: { action: 'onSubmit' },
+ },
+ decorators: [
+ (Story) => (
+
+ {Story()}
+
+
+ ),
+ ],
+}
+
+export default meta
+
+export const Empty = {
+ args: {
+ porting: portingFactory.build({
+ required: ['donorProviderApproval'],
+ }),
+ },
+}
+
+export const Prefilled = {
+ args: {
+ porting: portingFactory.build({
+ required: ['donorProviderApproval'],
+ donorProviderApproval: true,
+ }),
+ },
+}
diff --git a/lib/PortingEmbed/__stories__/StepHolderDetailsForm.stories.tsx b/lib/PortingEmbed/__stories__/StepHolderDetailsForm.stories.tsx
new file mode 100644
index 0000000..0070bd5
--- /dev/null
+++ b/lib/PortingEmbed/__stories__/StepHolderDetailsForm.stories.tsx
@@ -0,0 +1,47 @@
+import { Meta } from '@storybook/preact'
+
+import { portingFactory } from '@/testing/factories/porting'
+
+import { StepHolderDetailsForm } from '../StepHolderDetailsForm'
+
+const meta: Meta = {
+ title: 'Porting/StepHolderDetailsForm',
+ component: StepHolderDetailsForm,
+ tags: ['autodocs'],
+ argTypes: {
+ porting: { control: 'object' },
+ onValidationChange: { action: 'onValidationChange' },
+ onSubmit: { action: 'onSubmit' },
+ },
+ decorators: [
+ (Story) => (
+
+ {Story()}
+
+
+ ),
+ ],
+}
+
+export default meta
+
+export const Empty = {
+ args: {
+ porting: portingFactory.build({
+ required: ['firstName', 'lastName', 'birthday'],
+ }),
+ },
+}
+
+export const Prefilled = {
+ args: {
+ porting: portingFactory.build({
+ required: ['firstName', 'lastName', 'birthday'],
+ firstName: 'Jerry',
+ lastName: 'Seinfeld',
+ birthday: '1954-04-29',
+ }),
+ },
+}
diff --git a/lib/PortingEmbed/__tests__/PortingEmbed.test.tsx b/lib/PortingEmbed/__tests__/PortingEmbed.test.tsx
index 9e8a40f..8fe2e07 100644
--- a/lib/PortingEmbed/__tests__/PortingEmbed.test.tsx
+++ b/lib/PortingEmbed/__tests__/PortingEmbed.test.tsx
@@ -1,14 +1,15 @@
-import { render, screen } from '@testing-library/preact'
+import { render } from '@testing-library/preact'
import { portingFactory } from '@/testing/factories/porting'
import { PortingEmbed } from '../PortingEmbed'
-it('renders a form', () => {
- const porting = portingFactory.params({ id: 'prt_123' }).build()
+it('renders the root', () => {
+ const porting = portingFactory.build({
+ required: ['accountNumber', 'accountPin'],
+ })
render()
- const form = screen.getByRole('form')
- expect(form).toBeInTheDocument()
-})
-// TODO: different forms based on required fields
+ // stable class, used for in other tests
+ expect(document.querySelector('.__ge_portingRoot')).toBeInTheDocument()
+})
diff --git a/lib/PortingEmbed/__tests__/PortingForm.test.tsx b/lib/PortingEmbed/__tests__/PortingForm.test.tsx
index d366733..a4473a3 100644
--- a/lib/PortingEmbed/__tests__/PortingForm.test.tsx
+++ b/lib/PortingEmbed/__tests__/PortingForm.test.tsx
@@ -8,78 +8,158 @@ import { PortingForm } from '../PortingForm'
const wrapper = ({ children }: { children: React.ReactNode }) => {
return (
- {children}
+
{children}
)
}
-it('can enter and submit', async () => {
- const user = userEvent.setup()
+const validationChange = vi.fn()
+const submit = vi.fn()
+
+it('renders nothing if there are no steps left', () => {
const porting = portingFactory.build()
- const submit = vi.fn()
- render(, { wrapper })
-
- await user.type(screen.getByLabelText('Account Number'), '1234')
- await user.type(screen.getByLabelText('Account PIN'), '0000')
- await user.type(screen.getByLabelText('Birthday'), '01.01.1990')
- await user.type(screen.getByLabelText('First Name'), 'Jerry')
- await user.type(screen.getByLabelText('Last Name'), 'Seinfeld')
- await user.click(screen.getByRole('button', { name: 'Submit' }))
-
- expect(submit).toHaveBeenCalledWith({
- accountNumber: '1234',
- accountPin: '0000',
- birthday: '01.01.1990',
- firstName: 'Jerry',
- lastName: 'Seinfeld',
- })
+ render(
+ ,
+ { wrapper },
+ )
+
+ expect(screen.getByTestId('embed')).toBeEmptyDOMElement()
})
-describe('with existing porting fields', () => {
- it('prefills the inputs', async () => {
- const porting = portingFactory.build({
- accountNumber: '1234',
- accountPinExists: true,
- birthday: '01.01.1990',
- firstName: 'Jerry',
- lastName: 'Seinfeld',
+describe('carrier details', () => {
+ const porting = portingFactory.build({
+ required: ['accountNumber', 'accountPin'],
+ })
+
+ it('renders the corresponding form', () => {
+ render(
+ ,
+ { wrapper },
+ )
+ expect(screen.getByRole('form')).toBeInTheDocument()
+ expect(screen.getByLabelText('Account Number')).toBeInTheDocument()
+ expect(screen.getByLabelText('Account PIN')).toBeInTheDocument()
+ })
+
+ it('forwards the submitted data', async () => {
+ const user = userEvent.setup()
+ render(
+ ,
+ { wrapper },
+ )
+
+ await user.type(screen.getByLabelText('Account Number'), '123456')
+ await user.type(screen.getByLabelText('Account PIN'), '1234')
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+
+ expect(submit).toHaveBeenCalledWith({
+ accountNumber: '123456',
+ accountPin: '1234',
})
- const submit = vi.fn()
- render(, { wrapper })
-
- expect(screen.getByLabelText('Account Number')).toHaveValue('1234')
- expect(screen.getByLabelText('Birthday')).toHaveValue('01.01.1990')
- expect(screen.getByLabelText('First Name')).toHaveValue('Jerry')
- expect(screen.getByLabelText('Last Name')).toHaveValue('Seinfeld')
-
- // The account pin is not stored and cannot be pre-filled.
- // The presence of the account pin is instead indicated with a placeholder.
- expect(screen.getByLabelText('Account PIN')).toHaveValue('')
- expect(screen.getByLabelText('Account PIN')).toHaveAttribute(
- 'placeholder',
- '••••',
+ })
+})
+
+describe('holder details', () => {
+ const porting = portingFactory.build({
+ required: ['firstName', 'lastName', 'birthday'],
+ })
+
+ it('renders the corresponding form', () => {
+ render(
+ ,
+ { wrapper },
)
+ expect(screen.getByRole('form')).toBeInTheDocument()
+ expect(screen.getByLabelText('First Name')).toBeInTheDocument()
+ expect(screen.getByLabelText('Last Name')).toBeInTheDocument()
+ expect(screen.getByLabelText('Birthday')).toBeInTheDocument()
})
- it('only submits changed fields', async () => {
+ it('forwards the submitted data', async () => {
const user = userEvent.setup()
- const porting = portingFactory.build({
- accountNumber: '1234',
- accountPinExists: true,
- birthday: '01.01.1990',
- firstName: 'Jerry',
- lastName: 'Seinfeld',
+ render(
+ ,
+ { wrapper },
+ )
+
+ await user.type(screen.getByLabelText('First Name'), 'first')
+ await user.type(screen.getByLabelText('Last Name'), 'last')
+ await user.type(screen.getByLabelText('Birthday'), '1954-04-29')
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+
+ expect(submit).toHaveBeenCalledWith({
+ firstName: 'first',
+ lastName: 'last',
+ birthday: '1954-04-29',
})
- const submit = vi.fn()
- render(, { wrapper })
+ })
+})
+
+describe('address', () => {
+ const porting = portingFactory.build({
+ required: ['address'],
+ })
+
+ it('renders the corresponding form', () => {
+ render(
+ ,
+ { wrapper },
+ )
+ expect(screen.getByRole('form')).toBeInTheDocument()
+ expect(screen.getByLabelText('Line 1')).toBeInTheDocument()
+ })
+
+ it('forwards the submitted data', async () => {
+ const user = userEvent.setup()
+ render(
+ ,
+ { wrapper },
+ )
- await user.clear(screen.getByLabelText('Account Number'))
- await user.type(screen.getByLabelText('Account Number'), '5678')
+ await user.type(screen.getByLabelText('Line 1'), 'line1')
+ await user.type(screen.getByLabelText('City'), 'city')
+ await user.type(screen.getByLabelText('Postal Code'), 'pc123')
+ await user.type(screen.getByLabelText(/Country/), 'co')
await user.click(screen.getByRole('button', { name: 'Submit' }))
expect(submit).toHaveBeenCalledWith({
- accountNumber: '5678',
+ address: {
+ line1: 'line1',
+ line2: null,
+ city: 'city',
+ postalCode: 'pc123',
+ state: null,
+ country: 'CO',
+ },
})
})
})
diff --git a/lib/PortingEmbed/__tests__/StepAddressForm.test.tsx b/lib/PortingEmbed/__tests__/StepAddressForm.test.tsx
new file mode 100644
index 0000000..d5a34b8
--- /dev/null
+++ b/lib/PortingEmbed/__tests__/StepAddressForm.test.tsx
@@ -0,0 +1,410 @@
+import { render, screen } from '@testing-library/preact'
+import userEvent from '@testing-library/user-event'
+
+import { portingFactory } from '@/testing/factories/porting'
+
+import { StepAddressForm } from '../StepAddressForm'
+
+const wrapper = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+
+ )
+}
+
+const address = {
+ line1: 'line1',
+ line2: 'line2',
+ city: 'city',
+ postalCode: 'pc123',
+ state: 'ST',
+ country: 'CO',
+}
+
+describe('line1', () => {
+ it('exists', async () => {
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Line 1')).toBeInTheDocument()
+ })
+
+ it('can be prefilled', () => {
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { line1: 'line1' },
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Line 1')).toHaveValue('line1')
+ })
+
+ it('is trimmed', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText('Line 1'), ' line1 ')
+ expect(screen.getByLabelText('Line 1')).toHaveValue('line1 ')
+ })
+
+ it('is required', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, line1: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/Line 1 is required/)).toBeInTheDocument()
+ })
+
+ it('can be submitted', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, line1: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText('Line 1'), 'newline1')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ ...address, line1: 'newline1' })
+ })
+})
+
+describe('line2', () => {
+ it('exists', async () => {
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Line 2')).toBeInTheDocument()
+ })
+
+ it('can be prefilled', () => {
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { line2: 'line2' },
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Line 2')).toHaveValue('line2')
+ })
+
+ it('is trimmed', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText('Line 2'), ' line2 ')
+ expect(screen.getByLabelText('Line 2')).toHaveValue('line2 ')
+ })
+
+ it('is not required', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, line2: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ ...address, line2: null })
+ })
+
+ it('can be submitted', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, line2: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText('Line 2'), 'newline2')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ ...address, line2: 'newline2' })
+ })
+})
+
+describe('city', () => {
+ it('exists', async () => {
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('City')).toBeInTheDocument()
+ })
+
+ it('can be prefilled', () => {
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { city: 'city' },
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('City')).toHaveValue('city')
+ })
+
+ it('is trimmed', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText('City'), ' city ')
+ expect(screen.getByLabelText('City')).toHaveValue('city ')
+ })
+
+ it('is required', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, city: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/City is required/)).toBeInTheDocument()
+ })
+
+ it('can be submitted', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, city: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText('City'), 'newcity')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ ...address, city: 'newcity' })
+ })
+})
+
+describe('postal code', () => {
+ it('exists', async () => {
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Postal Code')).toBeInTheDocument()
+ })
+
+ it('can be prefilled', () => {
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { postalCode: 'pc123' },
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Postal Code')).toHaveValue('pc123')
+ })
+
+ it('is trimmed', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText('Postal Code'), ' pc123 ')
+ expect(screen.getByLabelText('Postal Code')).toHaveValue('pc123 ')
+ })
+
+ it('is required', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, postalCode: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/Postal Code is required/)).toBeInTheDocument()
+ })
+
+ it('can be submitted', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, postalCode: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText('Postal Code'), 'newpc123')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ ...address, postalCode: 'newpc123' })
+ })
+})
+
+describe('state', () => {
+ it('exists', async () => {
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText(/State/)).toBeInTheDocument()
+ })
+
+ it('can be prefilled', () => {
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { state: 'state' },
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText(/State/)).toHaveValue('state')
+ })
+
+ it('is trimmed and uppercased', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText(/State/), ' st ')
+ expect(screen.getByLabelText(/State/)).toHaveValue('ST ')
+ })
+
+ it('must be an ISO code', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText(/State/), 'hello world')
+ await user.click(screen.getByRole('button'))
+ expect(screen.getByText(/must be an iso state code/i)).toBeInTheDocument()
+ })
+
+ it('is not required', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, state: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ ...address, state: null })
+ })
+
+ it('can be submitted', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, state: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText(/State/), 'nst')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ ...address, state: 'NST' })
+ })
+})
+
+describe('country', () => {
+ it('exists', async () => {
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText(/Country/)).toBeInTheDocument()
+ })
+
+ it('can be prefilled', () => {
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { country: 'CO' },
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText(/Country/)).toHaveValue('CO')
+ })
+
+ it('is trimmed and uppercased', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText(/Country/), ' co ')
+ expect(screen.getByLabelText(/Country/)).toHaveValue('CO ')
+ })
+
+ it('must be an ISO code', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({ required: ['address'] })
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText(/Country/), 'hello world')
+ await user.click(screen.getByRole('button'))
+ expect(screen.getByText(/must be an iso country code/i)).toBeInTheDocument()
+ })
+
+ it('is required', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, country: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/Country is required/)).toBeInTheDocument()
+ })
+
+ it('can be submitted', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['address'],
+ address: { ...address, country: undefined },
+ })
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+ await user.type(screen.getByLabelText(/Country/), 'nc')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ ...address, country: 'NC' })
+ })
+})
diff --git a/lib/PortingEmbed/__tests__/StepCarrierDetailsForm.test.tsx b/lib/PortingEmbed/__tests__/StepCarrierDetailsForm.test.tsx
new file mode 100644
index 0000000..0d86f85
--- /dev/null
+++ b/lib/PortingEmbed/__tests__/StepCarrierDetailsForm.test.tsx
@@ -0,0 +1,242 @@
+import { render, screen } from '@testing-library/preact'
+import userEvent from '@testing-library/user-event'
+
+import { portingFactory } from '@/testing/factories/porting'
+
+import { StepCarrierDetailsForm } from '../StepCarrierDetailsForm'
+
+const wrapper = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+
+ )
+}
+
+describe('account number', () => {
+ it('is shown when required', () => {
+ const porting = portingFactory.build({ required: ['accountNumber'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Account Number')).toBeInTheDocument()
+ })
+
+ it('is hidden when not required', () => {
+ const porting = portingFactory.build({ required: [] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.queryByLabelText('Account Number')).not.toBeInTheDocument()
+ })
+
+ it('prefills the existing value', () => {
+ const porting = portingFactory.build({
+ required: ['accountNumber'],
+ accountNumber: '123456',
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Account Number')).toHaveValue('123456')
+ })
+
+ it('trims the value while entering data', async () => {
+ const porting = portingFactory.build({ required: ['accountNumber'] })
+ const user = userEvent.setup()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Account Number'), ' 1234 56 ')
+ // still allows users to type spaces at the end, otherwise you can't type
+ // any spaces
+ expect(screen.getByLabelText('Account Number')).toHaveValue('1234 56 ')
+ })
+
+ it('trims the value when submitting', async () => {
+ const porting = portingFactory.build({ required: ['accountNumber'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Account Number'), ' 1234 56 ')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ accountNumber: '1234 56' })
+ })
+
+ it('shows an error on submit when left empty', async () => {
+ const porting = portingFactory.build({ required: ['accountNumber'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(
+ screen.getByText(/the account number is required/i),
+ ).toBeInTheDocument()
+ })
+
+ it('cannot be submitted with only spaces', async () => {
+ const porting = portingFactory.build({ required: ['accountNumber'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Account Number'), ' ')
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(
+ screen.getByText(/the account number is required/i),
+ ).toBeInTheDocument()
+ })
+
+ it('fires validation events', async () => {
+ const porting = portingFactory.build({ required: ['accountNumber'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ const validate = vi.fn()
+ render(
+ ,
+ {
+ wrapper,
+ },
+ )
+
+ // initially valid because no validation was done yet
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ await user.clear(screen.getByLabelText('Account Number'))
+ await user.tab()
+ expect(validate).toHaveBeenLastCalledWith({ isValid: false })
+ await user.type(screen.getByLabelText('Account Number'), '123456')
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ })
+})
+
+describe('account pin', () => {
+ it('is shown when required', () => {
+ const porting = portingFactory.build({ required: ['accountPin'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Account PIN')).toBeInTheDocument()
+ })
+
+ it('is hidden when not required', () => {
+ const porting = portingFactory.build({ required: [] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.queryByLabelText('Account PIN')).not.toBeInTheDocument()
+ })
+
+ it('indicates the PIN already exists', () => {
+ const porting = portingFactory.build({
+ required: ['accountPin'],
+ accountPinExists: true,
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Account PIN')).toHaveValue('')
+ expect(screen.getByLabelText('Account PIN')).toHaveAttribute(
+ 'placeholder',
+ '••••',
+ )
+ })
+
+ it('trims the value when submitting', async () => {
+ const porting = portingFactory.build({ required: ['accountPin'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Account PIN'), ' 1234 56 ')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ accountPin: '1234 56' })
+ })
+
+ it('shows an error on submit when left empty and not present', async () => {
+ const porting = portingFactory.build({ required: ['accountPin'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/the account pin is required/i)).toBeInTheDocument()
+ })
+
+ it('can be left empty and submitted when already present', async () => {
+ const porting = portingFactory.build({
+ required: ['accountPin'],
+ accountPinExists: true,
+ })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({}) // account pin must not be in submit response
+ })
+
+ it('cannot be submitted with only spaces', async () => {
+ const porting = portingFactory.build({
+ required: ['accountPin'],
+ accountPinExists: true,
+ })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Account PIN'), ' ')
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(
+ screen.getByText(/the new account pin is empty/i),
+ ).toBeInTheDocument()
+ })
+
+ it('fires validation events', async () => {
+ const porting = portingFactory.build({ required: ['accountPin'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ const validate = vi.fn()
+ render(
+ ,
+ { wrapper },
+ )
+
+ // initially valid because no validation was done yet
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ await user.clear(screen.getByLabelText('Account PIN'))
+ await user.tab()
+ expect(validate).toHaveBeenLastCalledWith({ isValid: false })
+ await user.type(screen.getByLabelText('Account PIN'), '123456')
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ })
+})
diff --git a/lib/PortingEmbed/__tests__/StepDonorProviderApprovalForm.test.tsx b/lib/PortingEmbed/__tests__/StepDonorProviderApprovalForm.test.tsx
new file mode 100644
index 0000000..b35bf8a
--- /dev/null
+++ b/lib/PortingEmbed/__tests__/StepDonorProviderApprovalForm.test.tsx
@@ -0,0 +1,114 @@
+import { render, screen } from '@testing-library/preact'
+import userEvent from '@testing-library/user-event'
+
+import { portingFactory } from '@/testing/factories/porting'
+
+import { StepDonorProviderApprovalForm } from '../StepDonorProviderApprovalForm'
+
+const wrapper = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+
+ )
+}
+
+it('shows a checkbox', () => {
+ const porting = portingFactory.build({ required: ['donorProviderApproval'] })
+ render(
+ ,
+ { wrapper },
+ )
+ expect(
+ screen.getByLabelText(/i have notified my current provider/i),
+ ).toBeInTheDocument()
+})
+
+it('is initially unchecked', () => {
+ const porting = portingFactory.build({ required: ['donorProviderApproval'] })
+ render(
+ ,
+ { wrapper },
+ )
+ expect(
+ screen.getByLabelText(/i have notified my current provider/i),
+ ).not.toBeChecked()
+})
+
+it('prefills the existing value', () => {
+ const porting = portingFactory.build({
+ required: ['donorProviderApproval'],
+ donorProviderApproval: true,
+ })
+ render(
+ ,
+ {
+ wrapper,
+ },
+ )
+ expect(
+ screen.getByLabelText(/i have notified my current provider/i),
+ ).toBeChecked()
+})
+
+it('can be checked and submitted', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['donorProviderApproval'],
+ })
+ const submit = vi.fn()
+ render(
+ ,
+ { wrapper },
+ )
+
+ const checkbox = screen.getByLabelText(/i have notified my current provider/i)
+ await user.click(checkbox)
+ expect(checkbox).toBeChecked()
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+ expect(submit).toHaveBeenCalledWith({ donorProviderApproval: true })
+})
+
+it('is required to be checked', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['donorProviderApproval'],
+ })
+ const submit = vi.fn()
+ render(
+ ,
+ { wrapper },
+ )
+
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/you must get the approval/i)).toBeInTheDocument()
+})
+
+it('fires validation events', async () => {
+ const user = userEvent.setup()
+ const porting = portingFactory.build({
+ required: ['donorProviderApproval'],
+ })
+ const submit = vi.fn()
+ const validate = vi.fn()
+ render(
+ ,
+ { wrapper },
+ )
+
+ const checkbox = screen.getByText(/i have notified my current provider/i)
+
+ // initially valid because no validation was done yet
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ await user.click(checkbox)
+ await user.click(checkbox)
+ expect(validate).toHaveBeenLastCalledWith({ isValid: false })
+ await user.click(checkbox)
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+})
diff --git a/lib/PortingEmbed/__tests__/StepHolderDetailsForm.test.tsx b/lib/PortingEmbed/__tests__/StepHolderDetailsForm.test.tsx
new file mode 100644
index 0000000..e8f8001
--- /dev/null
+++ b/lib/PortingEmbed/__tests__/StepHolderDetailsForm.test.tsx
@@ -0,0 +1,323 @@
+import { render, screen } from '@testing-library/preact'
+import userEvent from '@testing-library/user-event'
+
+import { portingFactory } from '@/testing/factories/porting'
+
+import { StepHolderDetailsForm } from '../StepHolderDetailsForm'
+
+const wrapper = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+
+ )
+}
+
+describe('first name', () => {
+ it('is shown when required', () => {
+ const porting = portingFactory.build({ required: ['firstName'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('First Name')).toBeInTheDocument()
+ })
+
+ it('is hidden when not required', () => {
+ const porting = portingFactory.build({ required: [] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.queryByLabelText('First Name')).not.toBeInTheDocument()
+ })
+
+ it('prefills the existing value', () => {
+ const porting = portingFactory.build({
+ required: ['firstName'],
+ firstName: 'first',
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('First Name')).toHaveValue('first')
+ })
+
+ it('trims the value while entering data', async () => {
+ const porting = portingFactory.build({ required: ['firstName'] })
+ const user = userEvent.setup()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('First Name'), ' fir st ')
+ // still allows users to type spaces at the end, otherwise you can't type
+ // any spaces
+ expect(screen.getByLabelText('First Name')).toHaveValue('fir st ')
+ })
+
+ it('trims the value when submitting', async () => {
+ const porting = portingFactory.build({ required: ['firstName'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('First Name'), ' fir st ')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ firstName: 'fir st' })
+ })
+
+ it('shows an error on submit when left empty', async () => {
+ const porting = portingFactory.build({ required: ['firstName'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/first name is required/i)).toBeInTheDocument()
+ })
+
+ it('cannot be submitted with only spaces', async () => {
+ const porting = portingFactory.build({ required: ['firstName'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('First Name'), ' ')
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/first name is required/i)).toBeInTheDocument()
+ })
+
+ it('fires validation events', async () => {
+ const porting = portingFactory.build({ required: ['firstName'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ const validate = vi.fn()
+ render(
+ ,
+ {
+ wrapper,
+ },
+ )
+
+ // initially valid because no validation was done yet
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ await user.clear(screen.getByLabelText('First Name'))
+ await user.tab()
+ expect(validate).toHaveBeenLastCalledWith({ isValid: false })
+ await user.type(screen.getByLabelText('First Name'), 'first')
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ })
+})
+
+describe('last name', () => {
+ it('is shown when required', () => {
+ const porting = portingFactory.build({ required: ['lastName'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Last Name')).toBeInTheDocument()
+ })
+
+ it('is hidden when not required', () => {
+ const porting = portingFactory.build({ required: [] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.queryByLabelText('Last Name')).not.toBeInTheDocument()
+ })
+
+ it('prefills the existing value', () => {
+ const porting = portingFactory.build({
+ required: ['lastName'],
+ lastName: 'last',
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Last Name')).toHaveValue('last')
+ })
+
+ it('trims the value while entering data', async () => {
+ const porting = portingFactory.build({ required: ['lastName'] })
+ const user = userEvent.setup()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Last Name'), ' la st ')
+ // still allows users to type spaces at the end, otherwise you can't type
+ // any spaces
+ expect(screen.getByLabelText('Last Name')).toHaveValue('la st ')
+ })
+
+ it('trims the value when submitting', async () => {
+ const porting = portingFactory.build({ required: ['lastName'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Last Name'), ' la st ')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ lastName: 'la st' })
+ })
+
+ it('shows an error on submit when left empty', async () => {
+ const porting = portingFactory.build({ required: ['lastName'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/last name is required/i)).toBeInTheDocument()
+ })
+
+ it('cannot be submitted with only spaces', async () => {
+ const porting = portingFactory.build({ required: ['lastName'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Last Name'), ' ')
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/last name is required/i)).toBeInTheDocument()
+ })
+
+ it('fires validation events', async () => {
+ const porting = portingFactory.build({ required: ['lastName'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ const validate = vi.fn()
+ render(
+ ,
+ {
+ wrapper,
+ },
+ )
+
+ // initially valid because no validation was done yet
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ await user.clear(screen.getByLabelText('Last Name'))
+ await user.tab()
+ expect(validate).toHaveBeenLastCalledWith({ isValid: false })
+ await user.type(screen.getByLabelText('Last Name'), 'last')
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ })
+})
+
+describe('birthday', () => {
+ it('is shown when required', () => {
+ const porting = portingFactory.build({ required: ['birthday'] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Birthday')).toBeInTheDocument()
+ })
+
+ it('is hidden when not required', () => {
+ const porting = portingFactory.build({ required: [] })
+ render(, {
+ wrapper,
+ })
+ expect(screen.queryByLabelText('Birthday')).not.toBeInTheDocument()
+ })
+
+ it('prefills the existing value', () => {
+ const porting = portingFactory.build({
+ required: ['birthday'],
+ birthday: '1954-04-29',
+ })
+ render(, {
+ wrapper,
+ })
+ expect(screen.getByLabelText('Birthday')).toHaveValue('1954-04-29')
+ })
+
+ it('shows an error on submit when left empty', async () => {
+ const porting = portingFactory.build({ required: ['birthday'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/birthday is required/i)).toBeInTheDocument()
+ })
+
+ it('can enter and submit a valid date', async () => {
+ const porting = portingFactory.build({ required: ['birthday'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Birthday'), '1954-04-29')
+ await user.click(screen.getByRole('button'))
+ expect(submit).toHaveBeenCalledWith({ birthday: '1954-04-29' })
+ })
+
+ it('shows an error on submit with an invalid date', async () => {
+ const porting = portingFactory.build({ required: ['birthday'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ render(, {
+ wrapper,
+ })
+
+ await user.type(screen.getByLabelText('Birthday'), '1954-04')
+ await user.click(screen.getByRole('button'))
+ expect(submit).not.toHaveBeenCalled()
+ expect(screen.getByText(/birthday is required/i)).toBeInTheDocument()
+ })
+
+ it('fires validation events', async () => {
+ const porting = portingFactory.build({ required: ['birthday'] })
+ const user = userEvent.setup()
+ const submit = vi.fn()
+ const validate = vi.fn()
+ render(
+ ,
+ {
+ wrapper,
+ },
+ )
+
+ // initially valid because no validation was done yet
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ await user.clear(screen.getByLabelText('Birthday'))
+ await user.tab()
+ expect(validate).toHaveBeenLastCalledWith({ isValid: false })
+ await user.type(screen.getByLabelText('Birthday'), '1954-04-29')
+ expect(validate).toHaveBeenLastCalledWith({ isValid: true })
+ })
+})
diff --git a/lib/PortingEmbed/__tests__/index.test.tsx b/lib/PortingEmbed/__tests__/index.test.tsx
index 7981956..86e0433 100644
--- a/lib/PortingEmbed/__tests__/index.test.tsx
+++ b/lib/PortingEmbed/__tests__/index.test.tsx
@@ -12,7 +12,22 @@ import { PortingEmbed } from '../'
const project = 'test_project'
async function createFixtures() {
- const subscription = await subscriptionFactory.create()
+ const subscription = await subscriptionFactory
+ .associations({
+ porting: portingFactory
+ .params({
+ required: [
+ 'accountNumber',
+ 'accountPin',
+ 'firstName',
+ 'lastName',
+ 'address',
+ 'donorProviderApproval',
+ ],
+ })
+ .build(),
+ })
+ .create()
const connectSession = connectSessionFactory
.completePorting(subscription.id)
.build()
@@ -36,7 +51,7 @@ describe('mounting', () => {
const embed = await PortingEmbed(csn, { project })
embed.mount('#mount')
- expect(document.querySelector('.__gigsPortingEmbed')).toBeInTheDocument()
+ expect(document.querySelector('.__ge_portingRoot')).toBeInTheDocument()
})
it('mounts into a DOM element', async () => {
@@ -44,7 +59,7 @@ describe('mounting', () => {
const embed = await PortingEmbed(csn, { project })
embed.mount(document.getElementById('mount')!)
- expect(document.querySelector('.__gigsPortingEmbed')).toBeInTheDocument()
+ expect(document.querySelector('.__ge_portingRoot')).toBeInTheDocument()
})
})
@@ -55,7 +70,7 @@ describe('updating', () => {
embed.mount('#mount')
embed.update({})
- expect(document.querySelector('.__gigsPortingEmbed')).toBeInTheDocument()
+ expect(document.querySelector('.__ge_portingRoot')).toBeInTheDocument()
})
it('fails to update an unmounted embed', async () => {
@@ -190,7 +205,7 @@ describe('initialization', () => {
})
describe('updating a porting', () => {
- it('saves the updated data', async () => {
+ it('fires the submit status event', async () => {
const user = userEvent.setup()
const submitStatusEvent = vitest.fn()
@@ -201,9 +216,6 @@ describe('updating a porting', () => {
await user.type(screen.getByLabelText('Account Number'), '11880')
await user.type(screen.getByLabelText('Account PIN'), '1337')
- await user.type(screen.getByLabelText('Birthday'), '01.01.1990')
- await user.type(screen.getByLabelText('First Name'), 'Jane')
- await user.type(screen.getByLabelText('Last Name'), 'Doe')
await user.click(screen.getByRole('button', { name: 'Submit' }))
expect(submitStatusEvent).toHaveBeenCalledWith({ status: 'loading' })
@@ -213,19 +225,107 @@ describe('updating a porting', () => {
porting: expect.anything(),
}),
)
+ })
- const sub = db.subscriptions.find(
- (s) => s.id === csn.intent.completePorting.subscription,
- )
- const prt = db.portings.find((p) => p.id === sub!.porting!.id)
+ it('goes through all required steps', async () => {
+ const user = userEvent.setup()
+ const completedEvent = vitest.fn()
+
+ const csn = await createFixtures()
+ const embed = await PortingEmbed(csn, { project })
+ embed.mount('#mount')
+ embed.on('completed', completedEvent)
+
+ const getCurrentPorting = () => {
+ const sub = db.subscriptions.find(
+ (s) => s.id === csn.intent.completePorting.subscription,
+ )
+ const prt = db.portings.find((p) => p.id === sub!.porting!.id)
+ return prt
+ }
- expect(prt).toMatchObject({
+ await user.type(screen.getByLabelText('Account Number'), '11880')
+ await user.type(screen.getByLabelText('Account PIN'), '1337')
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+
+ await screen.findByLabelText('First Name')
+ expect(getCurrentPorting()).toMatchObject({
accountPinExists: true,
accountNumber: '11880',
- birthday: '01.01.1990',
- firstName: 'Jane',
- lastName: 'Doe',
+ firstName: null,
+ lastName: null,
+ birthday: null,
+ address: null,
+ donorProviderApproval: null,
})
+
+ await user.type(screen.getByLabelText('First Name'), 'first')
+ await user.type(screen.getByLabelText('Last Name'), 'last')
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+
+ await screen.findByLabelText('Line 1')
+ expect(getCurrentPorting()).toMatchObject({
+ accountPinExists: true,
+ accountNumber: '11880',
+ firstName: 'first',
+ lastName: 'last',
+ birthday: null,
+ address: null,
+ donorProviderApproval: null,
+ })
+
+ await user.type(screen.getByLabelText('Line 1'), 'line1')
+ await user.type(screen.getByLabelText('City'), 'city')
+ await user.type(screen.getByLabelText('Postal Code'), 'pc123')
+ await user.type(screen.getByLabelText(/Country/), 'co')
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+
+ await screen.findByLabelText(/i have notified my current/i)
+ expect(getCurrentPorting()).toMatchObject({
+ accountPinExists: true,
+ accountNumber: '11880',
+ firstName: 'first',
+ lastName: 'last',
+ birthday: null,
+ address: {
+ line1: 'line1',
+ line2: null,
+ city: 'city',
+ postalCode: 'pc123',
+ state: null,
+ country: 'CO',
+ },
+ donorProviderApproval: null,
+ })
+
+ await user.click(screen.getByLabelText(/i have notified my current/i))
+ expect(screen.getByLabelText(/i have notified my current/i)).toBeChecked()
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+
+ const finalPorting = {
+ accountPinExists: true,
+ accountNumber: '11880',
+ firstName: 'first',
+ lastName: 'last',
+ birthday: null,
+ address: {
+ line1: 'line1',
+ line2: null,
+ city: 'city',
+ postalCode: 'pc123',
+ state: null,
+ country: 'CO',
+ },
+ donorProviderApproval: true,
+ }
+
+ await waitFor(() => expect(completedEvent).toHaveBeenCalled())
+
+ expect(completedEvent).toHaveBeenCalledOnce()
+ expect(completedEvent).toHaveBeenCalledWith({
+ porting: expect.objectContaining(finalPorting),
+ })
+ expect(getCurrentPorting()).toMatchObject(finalPorting)
})
it('triggers an error event on error', async () => {
@@ -239,11 +339,7 @@ describe('updating a porting', () => {
// magic string in the mocked http handler
await user.type(screen.getByLabelText('Account Number'), 'MAGIC_FAIL')
-
await user.type(screen.getByLabelText('Account PIN'), '1337')
- await user.type(screen.getByLabelText('Birthday'), '01.01.1990')
- await user.type(screen.getByLabelText('First Name'), 'Jane')
- await user.type(screen.getByLabelText('Last Name'), 'Doe')
await user.click(screen.getByRole('button', { name: 'Submit' }))
await waitFor(() => {
diff --git a/lib/PortingEmbed/__tests__/wizardStep.test.ts b/lib/PortingEmbed/__tests__/wizardStep.test.ts
new file mode 100644
index 0000000..4073d37
--- /dev/null
+++ b/lib/PortingEmbed/__tests__/wizardStep.test.ts
@@ -0,0 +1,195 @@
+import { portingFactory } from '@/testing/factories/porting'
+
+import { wizardStep } from '../wizardStep'
+
+const basePorting = portingFactory
+
+describe('carrier details', () => {
+ it('detects required and missing account number', () => {
+ const porting = basePorting
+ .params({ required: ['accountNumber'], accountNumber: null })
+ .build()
+ expect(wizardStep(porting)).toBe('carrierDetails')
+ })
+
+ it('detects required and missing account pin', () => {
+ const porting = basePorting
+ .params({ required: ['accountPin'], accountPinExists: false })
+ .build()
+ expect(wizardStep(porting)).toBe('carrierDetails')
+ })
+
+ it('skips if fields are filled out', () => {
+ const porting = basePorting
+ .params({
+ required: ['accountNumber', 'accountPin'],
+ accountNumber: '123',
+ accountPinExists: true,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe(null)
+ })
+
+ it('is before holder details', () => {
+ const porting = basePorting
+ .params({
+ required: ['accountPin', 'firstName'],
+ accountPinExists: false,
+ firstName: null,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe('carrierDetails')
+ })
+})
+
+describe('holder details', () => {
+ it('detects required and missing firstName', () => {
+ const porting = basePorting
+ .params({ required: ['firstName'], firstName: null })
+ .build()
+ expect(wizardStep(porting)).toBe('holderDetails')
+ })
+
+ it('detects required and missing lastName', () => {
+ const porting = basePorting
+ .params({ required: ['lastName'], lastName: null })
+ .build()
+ expect(wizardStep(porting)).toBe('holderDetails')
+ })
+
+ it('detects required and missing birthday', () => {
+ const porting = basePorting
+ .params({ required: ['birthday'], birthday: null })
+ .build()
+ expect(wizardStep(porting)).toBe('holderDetails')
+ })
+
+ it('skips if fields are filled out', () => {
+ const porting = basePorting
+ .params({
+ required: ['firstName', 'lastName', 'birthday'],
+ firstName: 'first',
+ lastName: 'last',
+ birthday: 'birth',
+ })
+ .build()
+ expect(wizardStep(porting)).toBe(null)
+ })
+
+ it('is after carrier details', () => {
+ const porting = basePorting
+ .params({
+ required: ['accountNumber', 'accountPin', 'firstName'],
+ accountNumber: '123',
+ accountPinExists: true,
+ firstName: null,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe('holderDetails')
+ })
+
+ it('is before address', () => {
+ const porting = basePorting
+ .params({
+ required: ['firstName', 'lastName', 'birthday', 'address'],
+ firstName: null,
+ lastName: null,
+ birthday: null,
+ address: null,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe('holderDetails')
+ })
+})
+
+describe('address', () => {
+ it('detects required and missing address', () => {
+ const porting = basePorting
+ .params({ required: ['address'], address: null })
+ .build()
+ expect(wizardStep(porting)).toBe('address')
+ })
+
+ it('skips if fields are filled out', () => {
+ const porting = basePorting
+ .params({
+ required: ['address'],
+ address: {
+ city: 'city',
+ state: 'st',
+ country: 'co',
+ postalCode: '12345',
+ line1: 'line1',
+ },
+ })
+ .build()
+ expect(wizardStep(porting)).toBe(null)
+ })
+
+ it('is after holder details', () => {
+ const porting = basePorting
+ .params({
+ required: ['firstName', 'lastName', 'address'],
+ firstName: 'first',
+ lastName: 'last',
+ address: null,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe('address')
+ })
+
+ it('is before donor approval', () => {
+ const porting = basePorting
+ .params({
+ required: ['address', 'donorProviderApproval'],
+ address: null,
+ donorProviderApproval: null,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe('address')
+ })
+})
+
+describe('donor approval', () => {
+ it('detects required and missing donor approval', () => {
+ const porting = basePorting
+ .params({
+ required: ['donorProviderApproval'],
+ donorProviderApproval: null,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe('donorApproval')
+ })
+
+ it('skips if donor approval was set', () => {
+ const porting = basePorting
+ .params({
+ required: ['donorProviderApproval'],
+ donorProviderApproval: true,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe(null)
+ })
+
+ it('is after address', () => {
+ const porting = basePorting
+ .params({
+ required: ['address', 'donorProviderApproval'],
+ address: {
+ city: 'city',
+ state: 'st',
+ country: 'co',
+ postalCode: '12345',
+ line1: 'line1',
+ },
+ donorProviderApproval: null,
+ })
+ .build()
+ expect(wizardStep(porting)).toBe('donorApproval')
+ })
+})
+
+it('returns null if nothing is required', () => {
+ const porting = basePorting.params({ required: [] }).build()
+ expect(wizardStep(porting)).toBe(null)
+})
diff --git a/lib/PortingEmbed/index.tsx b/lib/PortingEmbed/index.tsx
index a4b9498..566fcc3 100644
--- a/lib/PortingEmbed/index.tsx
+++ b/lib/PortingEmbed/index.tsx
@@ -11,6 +11,7 @@ import {
PortingEmbed as PortingEmbedComponent,
ValidationChangeEvent,
} from './PortingEmbed'
+import { wizardStep } from './wizardStep'
type PortingEmbedInit = {
/** The ID of your Gigs project. */
@@ -23,9 +24,12 @@ type SubmitStatusEvent =
| { status: 'success'; porting: Porting }
| { status: 'error'; error: unknown }
+type CompletedEvent = { porting: Porting }
+
type Events = {
validationChange: ValidationChangeEvent
submitStatus: SubmitStatusEvent
+ completed: CompletedEvent
}
/**
@@ -99,6 +103,11 @@ export async function PortingEmbed(
project,
})
emitter.emit('submitStatus', { status: 'success', porting })
+
+ const step = wizardStep(porting)
+ if (step === null) {
+ emitter.emit('completed', { porting })
+ }
} catch (error) {
emitter.emit('submitStatus', { status: 'error', error })
} finally {
diff --git a/lib/PortingEmbed/wizardStep.ts b/lib/PortingEmbed/wizardStep.ts
new file mode 100644
index 0000000..241fcb0
--- /dev/null
+++ b/lib/PortingEmbed/wizardStep.ts
@@ -0,0 +1,63 @@
+import { Porting, PortingRequiredField } from '../types'
+
+export function wizardStep(porting: Porting) {
+ if (requiresCarrierDetails(porting)) {
+ return 'carrierDetails' as const
+ }
+
+ if (requiresHolderDetails(porting)) {
+ return 'holderDetails' as const
+ }
+
+ if (requiresAddress(porting)) {
+ return 'address' as const
+ }
+
+ if (requiresDonorApproval(porting)) {
+ return 'donorApproval' as const
+ }
+
+ return null
+}
+
+function requiresCarrierDetails(porting: Porting) {
+ if (requires(porting, 'accountPin') && !porting.accountPinExists) {
+ return true
+ }
+
+ if (requires(porting, 'accountNumber') && !porting.accountNumber) {
+ return true
+ }
+
+ return false
+}
+
+function requiresHolderDetails(porting: Porting) {
+ if (requires(porting, 'firstName') && !porting.firstName) {
+ return true
+ }
+
+ if (requires(porting, 'lastName') && !porting.lastName) {
+ return true
+ }
+
+ if (requires(porting, 'birthday') && !porting.birthday) {
+ return true
+ }
+
+ return false
+}
+
+function requiresAddress(porting: Porting) {
+ return requires(porting, 'address') && !porting.address
+}
+
+function requiresDonorApproval(porting: Porting) {
+ return (
+ requires(porting, 'donorProviderApproval') && !porting.donorProviderApproval
+ )
+}
+
+function requires(porting: Porting, field: PortingRequiredField) {
+ return porting.required.includes(field)
+}