* fix(white-label): back-to-clients link points at the byra cockpit's home domain A byra member working a company homed on another host (e.g. a pre-byra company on canonical) got a relative /clients on the wrong host instead of their white-label cockpit. resolveCockpitHref mirrors WL-14's home rule: relative when the current host is the cockpit's home (brand domain, or canonical for a brandless byra), else an absolute URL there. Cross- host links render a plain <a> with a 'Hanteras via' hint; the hop lands on the brand host's login (per-host sessions, WL-01) and WL-14 then lands on /clients. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(white-label): show the cross-host cockpit hint as visible text CodeRabbit: title-only hints never surface on touch devices, so the expanded-sidebar and mobile external back-links now render the 'Hanteras via {domain}' line as small muted text under the label (same pattern as the switcher's foreign entries). Also corrects the comments claiming the no-company branch never renders the back-link: it can, on its cockpit/settings surfaces, where the relative fallback matches pre-change behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
278 lines
7.8 KiB
TypeScript
278 lines
7.8 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
partitionCompaniesByHomeDomain,
|
|
isCompanyHomedOnHost,
|
|
isCockpitLandingRole,
|
|
resolveLandingPath,
|
|
resolveCockpitHref,
|
|
type TeamBrandRef,
|
|
} from '../home-domain'
|
|
|
|
interface Entry {
|
|
id: string
|
|
teamId: string | null
|
|
}
|
|
|
|
const CANONICAL = {
|
|
canonicalDomain: 'app.gnubok.se',
|
|
canonicalAppName: 'Accounted',
|
|
}
|
|
|
|
const siffraBrand: TeamBrandRef = { domain: 'app.siffra.se', appName: 'Siffra' }
|
|
|
|
function partition(opts: {
|
|
companies: Entry[]
|
|
brandByTeam?: Map<string, TeamBrandRef>
|
|
hostBrandTeamId?: string | null
|
|
}) {
|
|
return partitionCompaniesByHomeDomain<Entry>({
|
|
companies: opts.companies,
|
|
getTeamId: (c) => c.teamId,
|
|
brandByTeam: opts.brandByTeam ?? new Map(),
|
|
hostBrandTeamId: opts.hostBrandTeamId ?? null,
|
|
...CANONICAL,
|
|
})
|
|
}
|
|
|
|
describe('partitionCompaniesByHomeDomain', () => {
|
|
it('canonical host with no brands anywhere: everything visible (additive guarantee)', () => {
|
|
const result = partition({
|
|
companies: [
|
|
{ id: 'own', teamId: 'personal-1' },
|
|
{ id: 'other', teamId: null },
|
|
],
|
|
})
|
|
expect(result.visible.map((c) => c.id)).toEqual(['own', 'other'])
|
|
expect(result.foreign).toHaveLength(0)
|
|
})
|
|
|
|
it('canonical host: branded companies become foreign under their brand domain', () => {
|
|
const result = partition({
|
|
companies: [
|
|
{ id: 'own-firma', teamId: 'personal-1' },
|
|
{ id: 'client', teamId: 'byra-1' },
|
|
],
|
|
brandByTeam: new Map([['byra-1', siffraBrand]]),
|
|
})
|
|
expect(result.visible.map((c) => c.id)).toEqual(['own-firma'])
|
|
expect(result.foreign).toEqual([
|
|
{ item: { id: 'client', teamId: 'byra-1' }, domain: 'app.siffra.se', appName: 'Siffra' },
|
|
])
|
|
})
|
|
|
|
it("brand host: only the brand team's companies are visible", () => {
|
|
const result = partition({
|
|
companies: [
|
|
{ id: 'own-firma', teamId: 'personal-1' },
|
|
{ id: 'client-a', teamId: 'byra-1' },
|
|
{ id: 'client-b', teamId: 'byra-1' },
|
|
],
|
|
brandByTeam: new Map([['byra-1', siffraBrand]]),
|
|
hostBrandTeamId: 'byra-1',
|
|
})
|
|
expect(result.visible.map((c) => c.id)).toEqual(['client-a', 'client-b'])
|
|
// The consultant's own firma points home to the canonical domain.
|
|
expect(result.foreign).toEqual([
|
|
{
|
|
item: { id: 'own-firma', teamId: 'personal-1' },
|
|
domain: 'app.gnubok.se',
|
|
appName: 'Accounted',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('brand host: a company under ANOTHER brand points at that brand domain', () => {
|
|
const otherBrand: TeamBrandRef = { domain: 'app.other.se', appName: 'Other' }
|
|
const result = partition({
|
|
companies: [{ id: 'other-client', teamId: 'byra-2' }],
|
|
brandByTeam: new Map([
|
|
['byra-1', siffraBrand],
|
|
['byra-2', otherBrand],
|
|
]),
|
|
hostBrandTeamId: 'byra-1',
|
|
})
|
|
expect(result.visible).toHaveLength(0)
|
|
expect(result.foreign[0]!.domain).toBe('app.other.se')
|
|
})
|
|
})
|
|
|
|
describe('isCompanyHomedOnHost', () => {
|
|
const brandByTeam = new Map([['byra-1', siffraBrand]])
|
|
|
|
it('brandless company is homed on the canonical host', () => {
|
|
expect(
|
|
isCompanyHomedOnHost({
|
|
companyTeamId: 'personal-1',
|
|
brandByTeam,
|
|
hostBrandTeamId: null,
|
|
}),
|
|
).toBe(true)
|
|
})
|
|
|
|
it('branded company is NOT homed on the canonical host', () => {
|
|
expect(
|
|
isCompanyHomedOnHost({
|
|
companyTeamId: 'byra-1',
|
|
brandByTeam,
|
|
hostBrandTeamId: null,
|
|
}),
|
|
).toBe(false)
|
|
})
|
|
|
|
it('branded company is homed on its own brand host', () => {
|
|
expect(
|
|
isCompanyHomedOnHost({
|
|
companyTeamId: 'byra-1',
|
|
brandByTeam,
|
|
hostBrandTeamId: 'byra-1',
|
|
}),
|
|
).toBe(true)
|
|
})
|
|
|
|
it('brandless company is NOT homed on a brand host', () => {
|
|
expect(
|
|
isCompanyHomedOnHost({
|
|
companyTeamId: 'personal-1',
|
|
brandByTeam,
|
|
hostBrandTeamId: 'byra-1',
|
|
}),
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('isCockpitLandingRole', () => {
|
|
it('owner gets the automatic cockpit landing', () => {
|
|
expect(isCockpitLandingRole('owner')).toBe(true)
|
|
})
|
|
|
|
it('admin gets the automatic cockpit landing', () => {
|
|
expect(isCockpitLandingRole('admin')).toBe(true)
|
|
})
|
|
|
|
it('plain member lands like a regular user', () => {
|
|
expect(isCockpitLandingRole('member')).toBe(false)
|
|
})
|
|
|
|
it('unknown roles default to the regular landing (allowlist)', () => {
|
|
expect(isCockpitLandingRole('viewer')).toBe(false)
|
|
expect(isCockpitLandingRole('')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('resolveLandingPath', () => {
|
|
it("byrå staff on their brand host land in the cockpit", () => {
|
|
expect(
|
|
resolveLandingPath({
|
|
hostBrandTeamId: 'byra-1',
|
|
byraTeams: [{ teamId: 'byra-1', hasBrand: true }],
|
|
}),
|
|
).toBe('/clients')
|
|
})
|
|
|
|
it("someone else's brand host never lands in the cockpit", () => {
|
|
expect(
|
|
resolveLandingPath({
|
|
hostBrandTeamId: 'byra-2',
|
|
byraTeams: [{ teamId: 'byra-1', hasBrand: true }],
|
|
}),
|
|
).toBe('/')
|
|
})
|
|
|
|
it('brandless byrå homes its cockpit on the canonical host (WL-01)', () => {
|
|
expect(
|
|
resolveLandingPath({
|
|
hostBrandTeamId: null,
|
|
byraTeams: [{ teamId: 'byra-1', hasBrand: false }],
|
|
}),
|
|
).toBe('/clients')
|
|
})
|
|
|
|
it('a BRANDED byrå does not land in the cockpit on the canonical host', () => {
|
|
expect(
|
|
resolveLandingPath({
|
|
hostBrandTeamId: null,
|
|
byraTeams: [{ teamId: 'byra-1', hasBrand: true }],
|
|
}),
|
|
).toBe('/')
|
|
})
|
|
|
|
it('non-byrå users always land on / (byte-identical flow)', () => {
|
|
expect(resolveLandingPath({ hostBrandTeamId: null, byraTeams: [] })).toBe('/')
|
|
expect(resolveLandingPath({ hostBrandTeamId: 'byra-1', byraTeams: [] })).toBe('/')
|
|
})
|
|
})
|
|
|
|
describe('resolveCockpitHref', () => {
|
|
const brandByTeam = new Map([['byra-1', siffraBrand]])
|
|
const canonicalDomain = CANONICAL.canonicalDomain
|
|
|
|
it('brandless byrå on the canonical host stays relative', () => {
|
|
expect(
|
|
resolveCockpitHref({
|
|
byraTeamId: 'byra-1',
|
|
brandByTeam: new Map(),
|
|
hostBrandTeamId: null,
|
|
canonicalDomain,
|
|
}),
|
|
).toBe('/clients')
|
|
})
|
|
|
|
it('branded byrå on its own brand host stays relative', () => {
|
|
expect(
|
|
resolveCockpitHref({
|
|
byraTeamId: 'byra-1',
|
|
brandByTeam,
|
|
hostBrandTeamId: 'byra-1',
|
|
canonicalDomain,
|
|
}),
|
|
).toBe('/clients')
|
|
})
|
|
|
|
it('branded byrå on the canonical host points at the brand domain (problem 4)', () => {
|
|
expect(
|
|
resolveCockpitHref({
|
|
byraTeamId: 'byra-1',
|
|
brandByTeam,
|
|
hostBrandTeamId: null,
|
|
canonicalDomain,
|
|
}),
|
|
).toBe('https://app.siffra.se/clients')
|
|
})
|
|
|
|
it("branded byrå on someone ELSE's brand host points at its own brand domain", () => {
|
|
expect(
|
|
resolveCockpitHref({
|
|
byraTeamId: 'byra-1',
|
|
brandByTeam,
|
|
hostBrandTeamId: 'byra-2',
|
|
canonicalDomain,
|
|
}),
|
|
).toBe('https://app.siffra.se/clients')
|
|
})
|
|
|
|
it('brandless byrå on a foreign brand host points home to canonical (WL-14 symmetry)', () => {
|
|
expect(
|
|
resolveCockpitHref({
|
|
byraTeamId: 'byra-1',
|
|
brandByTeam: new Map(),
|
|
hostBrandTeamId: 'byra-2',
|
|
canonicalDomain,
|
|
}),
|
|
).toBe('https://app.gnubok.se/clients')
|
|
})
|
|
|
|
it('byrå with zero client companies still resolves via its own brand entry', () => {
|
|
// The layout adds the byrå team id to the resolveBrandsForTeams id list
|
|
// precisely so this map entry exists even when no membership company
|
|
// belongs to the byrå (pre-byrå companies have team_id null).
|
|
expect(
|
|
resolveCockpitHref({
|
|
byraTeamId: 'byra-1',
|
|
brandByTeam: new Map([['byra-1', siffraBrand]]),
|
|
hostBrandTeamId: null,
|
|
canonicalDomain,
|
|
}),
|
|
).toBe('https://app.siffra.se/clients')
|
|
})
|
|
})
|