Skip to content
73 changes: 73 additions & 0 deletions __tests__/e2e/admin/tenant-selector/dashboard.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, tenantSelectorTest as test } from '../../fixtures/tenant-selector.fixture'
import { AdminUrlUtil, CollectionSlugs } from '../../helpers'

/**
* Dashboard Tests
*
* Tests that the tenant selector is visible and functional on the admin dashboard.
* Previously, the selector was hidden on the dashboard because viewType was never
* set to 'dashboard'. Fixed by detecting dashboard from URL params instead.
*
* Related: https://github.com/NWACus/web/issues/691
*/

test.describe.configure({ timeout: 90000 })

test.describe('Tenant selector on dashboard', () => {
test('should be visible on the dashboard for super admin', async ({
loginAs,
isTenantSelectorVisible,
getTenantOptions,
}) => {
const page = await loginAs('superAdmin')
const url = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.pages)

await page.goto(url.dashboard)
await page.waitForLoadState('networkidle')

const isVisible = await isTenantSelectorVisible(page)
expect(isVisible).toBe(true)

const options = await getTenantOptions(page)
expect(options.length).toBeGreaterThanOrEqual(4)

await page.context().close()
})

test('should be visible on the dashboard for multi-tenant admin', async ({
loginAs,
isTenantSelectorVisible,
getTenantOptions,
}) => {
const page = await loginAs('multiTenantAdmin')
const url = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.pages)

await page.goto(url.dashboard)
await page.waitForLoadState('networkidle')

const isVisible = await isTenantSelectorVisible(page)
expect(isVisible).toBe(true)

const options = await getTenantOptions(page)
expect(options.length).toBe(2) // NWAC and SNFAC

await page.context().close()
})

test('should be hidden on dashboard for single-tenant users', async ({
loginAs,
isTenantSelectorVisible,
}) => {
const page = await loginAs('singleTenantAdmin')
const url = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.pages)

await page.goto(url.dashboard)
await page.waitForLoadState('networkidle')

// Single-tenant users have only 1 option, so selector is hidden
const isVisible = await isTenantSelectorVisible(page)
expect(isVisible).toBe(false)

await page.context().close()
})
})
137 changes: 137 additions & 0 deletions __tests__/e2e/admin/tenant-selector/sync-on-save.e2e.spec.ts
Comment thread
rchlfryn marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
VALID_TENANT_SLUGS,
type ValidTenantSlug,
} from '../../../../src/utilities/tenancy/avalancheCenters'
import { openNav } from '../../fixtures/nav.fixture'
import { expect, tenantSelectorTest as test } from '../../fixtures/tenant-selector.fixture'
import { AdminUrlUtil, CollectionSlugs, saveDocAndAssert, waitForFormReady } from '../../helpers'

/**
* Sync on Save Tests
*
* Tests that the tenant selector dropdown refreshes automatically when a
* tenant is saved (created or updated), without requiring a page reload.
*
* The SyncTenantsOnSave component watches useDocumentInfo().lastUpdateTime
* and calls syncTenants() when it changes.
*
* These tests run in serial order: test 1 creates a tenant, test 2 edits
* and then cleans it up.
*/

const TEMP_TENANT_NAME = 'E2E Sync Test Center'
const UPDATED_TENANT_NAME = 'E2E Sync Test Renamed'

test.describe.configure({ timeout: 90000, mode: 'serial' })

/** Find the first valid tenant slug that doesn't already exist in the database */
async function findUnusedTenantSlug(
page: import('@playwright/test').Page,
): Promise<ValidTenantSlug> {
const response = await page.request.get('http://localhost:3000/api/tenants?limit=100')
const data = await response.json()
const usedSlugs = new Set(data.docs?.map((doc: { slug: string }) => doc.slug))

const unused = VALID_TENANT_SLUGS.find((slug) => !usedSlugs.has(slug))
if (!unused) {
throw new Error('No unused tenant slugs available')
}
return unused
}

/** Delete a tenant by slug if it exists */
async function deleteTenantBySlug(page: import('@playwright/test').Page, slug: string) {
const response = await page.request.get(
`http://localhost:3000/api/tenants?where[slug][equals]=${slug}&limit=1`,
)
const data = await response.json()
if (data.docs?.[0]) {
await page.request.delete(`http://localhost:3000/api/tenants/${data.docs[0].id}`)
}
}

test.describe('Tenant selector syncs on save', () => {
let tempTenantSlug: ValidTenantSlug

test('should show new tenant in selector after creation', async ({
loginAs,
getTenantOptions,
}) => {
const page = await loginAs('superAdmin')
const tenantsUrl = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.tenants)

tempTenantSlug = await findUnusedTenantSlug(page)

try {
// Navigate to create a new tenant
await page.goto(tenantsUrl.create)
await page.waitForLoadState('networkidle')
await waitForFormReady(page)

await page.locator('#field-name').fill(TEMP_TENANT_NAME)
// Slug is a select dropdown — open it, filter, and click the matching option
const slugField = page.locator('#field-slug')
await slugField.locator('button.dropdown-indicator').click()
await slugField.locator('.rs__option', { hasText: tempTenantSlug }).click()
await saveDocAndAssert(page)

// Navigate to a tenant-scoped collection via client-side nav link (NOT page.goto)
await openNav(page)
await Promise.all([
page.waitForURL('**/admin/collections/pages'),
page.locator('nav a[href="/admin/collections/pages"]').click(),
])
await page.waitForLoadState('networkidle')

// The tenant selector should show the new tenant without a full page reload
const options = await getTenantOptions(page)
expect(options).toContain(TEMP_TENANT_NAME)
} finally {
await page.context().close()
}
})

test('should update tenant name in selector after editing', async ({
loginAs,
getTenantOptions,
}) => {
const page = await loginAs('superAdmin')
const tenantsUrl = new AdminUrlUtil('http://localhost:3000', CollectionSlugs.tenants)

try {
// Find the tenant created by the previous test
const response = await page.request.get(
`http://localhost:3000/api/tenants?where[slug][equals]=${tempTenantSlug}&limit=1`,
)
const data = await response.json()
const tenantId = data.docs?.[0]?.id
if (!tenantId) {
throw new Error(`Tenant with slug "${tempTenantSlug}" not found — test 1 may have failed`)
}

// Navigate to the tenant edit page
await page.goto(tenantsUrl.edit(tenantId))
await page.waitForLoadState('networkidle')
await waitForFormReady(page)

// Rename the tenant
await page.locator('#field-name').fill(UPDATED_TENANT_NAME)
await saveDocAndAssert(page)

// Navigate to a tenant-scoped collection via client-side nav link (NOT page.goto)
await openNav(page)
await Promise.all([
page.waitForURL('**/admin/collections/pages'),
page.locator('nav a[href="/admin/collections/pages"]').click(),
])
await page.waitForLoadState('networkidle')

const options = await getTenantOptions(page)
expect(options).toContain(UPDATED_TENANT_NAME)
expect(options).not.toContain(TEMP_TENANT_NAME)
} finally {
await deleteTenantBySlug(page, tempTenantSlug)
await page.context().close()
}
})
})
9 changes: 0 additions & 9 deletions docs/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,6 @@ This outlines steps required when a new center (tenant) comes on board. This doc
<td>Manual</td>
<td>Troubleshoot: <br />In Vercel, go to the project → Storage → production Edge Config and edit file</td>
</tr>
<tr>
<td rowspan="2">New tenant option shown in <code>TenantSelectionProvider</code></td>
<td>Manual</td>
<td>Refresh page</td>
</tr>
<tr>
<td>Future automation</td>
<td>Use <code>syncTenants</code></td>
</tr>
<tr>
<td>Create documents for Global Collections <ul><li>Website Settings</><li>Navigation</li><li>Home Pages</li></ul></td>
<td>Manual</td>
Expand Down
2 changes: 2 additions & 0 deletions src/app/(payload)/admin/importMap.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions src/collections/Tenants/components/SyncTenantsOnSave.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client'

import { useDocumentInfo } from '@payloadcms/ui'
import { useEffect, useRef } from 'react'

import { useTenantSelection } from '@/providers/TenantSelectionProvider/index.client'

/**
* Invisible component placed on the Tenants edit view that syncs the tenant
* selector dropdown whenever a tenant document is saved (created or updated).
*/
export const SyncTenantsOnSave = () => {
const { lastUpdateTime } = useDocumentInfo()
const { syncTenants } = useTenantSelection()
const prevUpdateTime = useRef(lastUpdateTime)

useEffect(() => {
if (lastUpdateTime !== prevUpdateTime.current) {
prevUpdateTime.current = lastUpdateTime
syncTenants()
}
}, [lastUpdateTime, syncTenants])

return null
}
7 changes: 7 additions & 0 deletions src/collections/Tenants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export const Tenants: CollectionConfig = {
useAsTitle: 'name',
group: 'Permissions',
hidden: ({ user }) => hasReadOnlyAccess(user, 'tenants'),
components: {
edit: {
beforeDocumentControls: [
'@/collections/Tenants/components/SyncTenantsOnSave#SyncTenantsOnSave',
],
},
},
},
labels: {
plural: 'Avalanche Centers',
Expand Down
Loading