mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a redesigned Partners page with partnership options, benefits, application steps, FAQs, integration resources, and featured partners. - Added an on-page partner application form with questions tailored to the selected partnership type. - Added a confirmation message with next steps and a link to the OAuth integration guide. - **Bug Fixes** - Updated partner application links to open the form on the Partners page. - Added support for checkbox-group fields in forms. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Alan Daniel <stylesshjs@gmail.com> Co-authored-by: Alex Hall <alex.hall@supabase.io> Co-authored-by: Dion Zeneli <101271736+Dionysos288@users.noreply.github.com>
170 lines
6.4 KiB
TypeScript
170 lines
6.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import hubspotSnapshotJson from './__generated__/partner-intake-form.hubspot.json'
|
|
import { fields } from './PartnerIntakeForm.fields'
|
|
import { staticFormCrmRegistry } from '@/lib/staticFormCrm'
|
|
|
|
// staticFormCrm.ts is `import 'server-only'` gated; that guard throws when
|
|
// bundled for a client, but under plain Node/Vitest (no Next.js webpack
|
|
// aliasing) it throws unconditionally, so it must be stubbed for this test.
|
|
vi.mock('server-only', () => ({}))
|
|
|
|
/**
|
|
* Cross-checks PartnerIntakeForm.tsx + staticFormCrm.ts against the
|
|
* generated HubSpot schema snapshot (apps/www/scripts/syncPartnerIntakeForm.mjs).
|
|
*
|
|
* This is the actual "can't silently drift" guarantee: re-run
|
|
* `pnpm run sync:partner-form` to refresh the snapshot from the live
|
|
* HubSpot form, and this test starts failing the moment the snapshot no
|
|
* longer matches the hand-authored form — forcing a deliberate update to
|
|
* PartnerIntakeForm.tsx / staticFormCrm.ts instead of a silent HubSpot
|
|
* submission failure.
|
|
*/
|
|
|
|
interface HubSpotFieldSnapshot {
|
|
name: string
|
|
label: string
|
|
fieldType: string
|
|
required: boolean
|
|
options: { label: string; value: string }[]
|
|
description?: string
|
|
dependsOn?: { field: string; values: string[] }
|
|
}
|
|
|
|
const hubspotSnapshot = hubspotSnapshotJson as {
|
|
portalId: number
|
|
formGuid: string
|
|
fields: HubSpotFieldSnapshot[]
|
|
}
|
|
|
|
const crmConfig = staticFormCrmRegistry['partners/become-a-partner']?.hubspot
|
|
if (!crmConfig) {
|
|
throw new Error(
|
|
"Expected a hubspot CRM config for 'partners/become-a-partner' in staticFormCrm.ts"
|
|
)
|
|
}
|
|
|
|
const fieldMap = crmConfig.fieldMap ?? {}
|
|
const excludeFields = new Set(crmConfig.excludeFields ?? [])
|
|
const hubspotFieldsByName = new Map(hubspotSnapshot.fields.map((f) => [f.name, f]))
|
|
|
|
// Company-object fields are bound via a `0-2/` prefix (see HubSpotClient) and
|
|
// aren't part of the Contact-field schema this endpoint returns — treat them
|
|
// as known rather than flagging them as unmapped.
|
|
const KNOWN_NON_CONTACT_FIELDS = new Set(['name', 'website'])
|
|
|
|
/** The HubSpot property name a given app field forwards to. */
|
|
function hubspotNameFor(appFieldName: string): string {
|
|
const mapped = fieldMap[appFieldName] ?? appFieldName
|
|
const slashIndex = mapped.indexOf('/')
|
|
return slashIndex === -1 ? mapped : mapped.slice(slashIndex + 1)
|
|
}
|
|
|
|
describe('PartnerIntakeForm HubSpot sync', () => {
|
|
it('maps every required, non-excluded HubSpot field to an app field', () => {
|
|
const forwardedHubspotNames = new Set(
|
|
fields.filter((f) => !excludeFields.has(f.name)).map((f) => hubspotNameFor(f.name))
|
|
)
|
|
|
|
const missing = hubspotSnapshot.fields
|
|
.filter((f) => f.required)
|
|
.map((f) => f.name)
|
|
.filter((name) => !forwardedHubspotNames.has(name))
|
|
|
|
expect(missing).toEqual([])
|
|
})
|
|
|
|
it('binds company name and website to the Company object', () => {
|
|
// The `0-2/` prefix makes HubSpotClient submit these against the Company
|
|
// object rather than the Contact — dropping it silently mis-files them.
|
|
expect(fieldMap.company_name).toBe('0-2/name')
|
|
expect(fieldMap.company_website).toBe('0-2/website')
|
|
})
|
|
|
|
it('keeps requiredness and conditional visibility in sync for every mapped field', () => {
|
|
// A field HubSpot requires but we don't (or one we only show for the wrong
|
|
// partner types) makes the HubSpot submission fail for some users only.
|
|
const mismatches: string[] = []
|
|
|
|
for (const field of fields) {
|
|
if (excludeFields.has(field.name)) continue
|
|
|
|
const hubspotName = hubspotNameFor(field.name)
|
|
const hubspotField = hubspotFieldsByName.get(hubspotName)
|
|
if (!hubspotField) continue
|
|
|
|
if (Boolean(field.required) !== hubspotField.required) {
|
|
mismatches.push(
|
|
`${field.name}: required is ${Boolean(field.required)}, HubSpot's ${hubspotName} is ${hubspotField.required}`
|
|
)
|
|
}
|
|
|
|
const ours = field.showWhen
|
|
? {
|
|
field: hubspotNameFor(field.showWhen.field),
|
|
values: [
|
|
...(field.showWhen.in ?? []),
|
|
...(field.showWhen.equals !== undefined ? [field.showWhen.equals] : []),
|
|
].sort(),
|
|
}
|
|
: undefined
|
|
const theirs = hubspotField.dependsOn
|
|
? { field: hubspotField.dependsOn.field, values: [...hubspotField.dependsOn.values].sort() }
|
|
: undefined
|
|
|
|
if (JSON.stringify(ours) !== JSON.stringify(theirs)) {
|
|
mismatches.push(
|
|
`${field.name}: shown when ${JSON.stringify(ours ?? 'always')}, HubSpot's ${hubspotName} when ${JSON.stringify(theirs ?? 'always')}`
|
|
)
|
|
}
|
|
}
|
|
|
|
expect(mismatches).toEqual([])
|
|
})
|
|
|
|
it('forwards every non-excluded app field to a HubSpot property that actually exists', () => {
|
|
// An app field forwarded to a HubSpot property HubSpot doesn't define
|
|
// causes the *entire* HubSpot submission to fail (see HubSpotClient).
|
|
const unknown = fields
|
|
.filter((f) => !excludeFields.has(f.name))
|
|
.map((f) => hubspotNameFor(f.name))
|
|
.filter((name) => !KNOWN_NON_CONTACT_FIELDS.has(name))
|
|
.filter((name) => !hubspotFieldsByName.has(name))
|
|
|
|
expect(unknown).toEqual([])
|
|
})
|
|
|
|
it('keeps option values in exact sync for every mapped select/checkbox-group field', () => {
|
|
// Checked in BOTH directions: a value we send that HubSpot doesn't
|
|
// recognize, and — the bug that slipped through when this only checked
|
|
// one direction — a live HubSpot option we're not offering at all (e.g.
|
|
// a "Marketplace Partner" option nobody could ever select).
|
|
const mismatches: string[] = []
|
|
|
|
for (const field of fields) {
|
|
if (field.type !== 'select' && field.type !== 'checkbox-group') continue
|
|
if (excludeFields.has(field.name)) continue
|
|
|
|
const hubspotName = hubspotNameFor(field.name)
|
|
const hubspotField = hubspotFieldsByName.get(hubspotName)
|
|
if (!hubspotField || hubspotField.options.length === 0) continue
|
|
|
|
const ourValues = new Set(field.options.map((o) => o.value))
|
|
const hubspotValues = new Set(hubspotField.options.map((o) => o.value))
|
|
|
|
for (const value of ourValues) {
|
|
if (!hubspotValues.has(value)) {
|
|
mismatches.push(`${field.name}: "${value}" is not a valid ${hubspotName} option`)
|
|
}
|
|
}
|
|
for (const value of hubspotValues) {
|
|
if (!ourValues.has(value)) {
|
|
mismatches.push(`${field.name}: missing HubSpot's "${value}" option for ${hubspotName}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(mismatches).toEqual([])
|
|
})
|
|
})
|