Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
// to four distinct copies. An account with no memberships gets the
// no-organization state whose action opens the web organizations page;
// memberships with nothing selected get the picker state whose action opens
// the context picker sheet; a deep-link override absent from the member list
// is access-denied; a fetch failure is a retryable QueryError; a still
// resolving context is a spinner. A stale persisted selection (no override)
// keeps the older "unavailable" copy.
// the context picker sheet; a deep-link override absent from a NON-empty
// member list is access-denied, while the same override against zero
// memberships is the no-organization state; a fetch failure is a retryable
// QueryError; a still resolving context is a spinner. A stale persisted
// selection (no override) keeps the older "unavailable" copy.

import { type ComponentType, createElement, type ReactNode } from 'react';
import { act, TestRenderer } from '@/test/renderer';
Expand Down Expand Up @@ -189,9 +190,13 @@ describe('OrganizationBoundary settled states', () => {
});
});

it('renders access-denied when an override org is absent from the member list', () => {
it('renders access-denied when an override org is absent from a non-empty member list', () => {
useOrgBoundaryMock.mockReturnValue(
boundaryState({ organizationId: 'org-missing', org: undefined })
boundaryState({
organizationId: 'org-missing',
org: undefined,
orgs: [{ organizationId: 'org-1', organizationName: 'Organization' }],
})
);

const renderer = mountBoundary('org-missing');
Expand Down Expand Up @@ -241,6 +246,38 @@ describe('OrganizationBoundary settled states', () => {
});
});

it('offers the web organizations page for a deep-link override with no memberships', () => {
useOrgBoundaryMock.mockReturnValue(
boundaryState({ organizationId: 'org-linked', org: undefined, orgs: [] })
);

const renderer = mountBoundary('org-linked');
const texts = collectText(renderer.toJSON());

expect(findByType(renderer.root, 'EmptyStateMock')).toHaveLength(1);
expect(texts).toContain('No organization');
expect(texts).toContain('Open organizations on web');
expect(texts).toContain('Back to profile');
expect(texts).not.toContain('Access denied');
expect(texts).not.toContain('Select an organization');

const webButton = renderer.root.find(
node =>
String(node.type) === 'ButtonMock' &&
collectText(node).includes('Open organizations on web')
);
act(() => {
(webButton.props.onPress as (() => void) | undefined)?.();
});
expect(openExternalUrlMock).toHaveBeenCalledWith('https://cloud.example.test/organizations', {
label: 'Open organizations on web',
});

act(() => {
renderer.unmount();
});
});

it('opens the context picker when memberships exist but nothing is selected', () => {
useOrgBoundaryMock.mockReturnValue(
boundaryState({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ type OrganizationBoundaryProps = Readonly<{
* to profile stays as the plain exit.
* - memberships exist but nothing is selected → the action opens the
* context picker sheet, the same account switcher the composer uses.
* - a deep-link override absent from the list is access-denied; a stale
* persisted selection keeps the older "unavailable" copy.
* - a deep-link override absent from a non-empty member list is
* access-denied; a stale persisted selection keeps the older
* "unavailable" copy. An absent override with zero memberships is the
* no-organization state above, not access-denied: the account has no
* organization to be denied access to.
* Calls `useOrgBoundary()` itself — cheap, context/query-cache backed — so
* callers only need to pass a `title` for full screens (sheets omit it).
*/
Expand Down Expand Up @@ -100,8 +103,7 @@ export function OrganizationBoundary({
</Button>
);
const hasOrganizations = (orgs?.length ?? 0) > 0;
const isAbsentOverride = organizationIdOverride != null && org == null;
if (!hasOrganizations && !isAbsentOverride) {
if (!hasOrganizations) {
const openWebOrganizations = () => {
void openExternalUrl(`${WEB_BASE_URL}/organizations`, {
label: t('organization.boundary.openWebOrganizations'),
Expand Down
Loading