diff --git a/apps/staged/src/lib/features/projects/ProjectCardLabels.component.test.ts b/apps/staged/src/lib/features/projects/ProjectCardLabels.component.test.ts index bda8d57d8..285bd6036 100644 --- a/apps/staged/src/lib/features/projects/ProjectCardLabels.component.test.ts +++ b/apps/staged/src/lib/features/projects/ProjectCardLabels.component.test.ts @@ -1,7 +1,9 @@ // Mounts a card's labels (the home grid's and the board's) over the real // tagsStore, with the backend commands and the badge store mocked: the -// project's tags sit as chips in the same cluster as its repos, and a board -// card leaves out the chip its column already stands for. +// project's repos show under their badges' short names (the full path in the +// tooltip, and in place of a repo with no badge), its tags sit as chips in +// the same cluster, and a board card leaves out the chip its column already +// stands for. import { flushSync, mount, tick, unmount } from 'svelte'; import { afterEach, describe, expect, it, vi } from 'vitest'; import type { Project, ProjectRepo, TagState } from '../../types'; @@ -9,7 +11,7 @@ import { tag, tagState } from '../tags/testStubs/tagFixtures'; const { backend, badges } = vi.hoisted(() => ({ backend: { state: null as unknown as TagState }, - /** Badges by `githubRepo`. */ + /** Badges by `githubRepo`, or `githubRepo:subpath` for a subpath's. */ badges: new Map(), })); @@ -27,7 +29,10 @@ vi.mock('../../stores/projectsData.svelte', () => ({ projectsDataStore: { liveProjects: [] }, })); vi.mock('../../stores/repoBadges.svelte', () => ({ - repoBadgeStore: { lookup: (githubRepo: string) => badges.get(githubRepo) ?? null }, + repoBadgeStore: { + lookup: (githubRepo: string, subpath: string | null | undefined) => + badges.get(subpath ? `${githubRepo}:${subpath}` : githubRepo) ?? null, + }, })); import { tagsStore } from '../../stores/tags.svelte'; @@ -45,9 +50,9 @@ const PROJECT: Project = { updatedAt: 1, }; -function repo(githubRepo: string): ProjectRepo { +function repo(githubRepo: string, overrides: Partial = {}): ProjectRepo { return { - id: githubRepo, + id: `${githubRepo}:${overrides.subpath ?? ''}`, projectId: 'p1', githubRepo, branchName: 'main', @@ -57,6 +62,7 @@ function repo(githubRepo: string): ProjectRepo { headRepo: null, createdAt: 1, updatedAt: 1, + ...overrides, }; } @@ -67,13 +73,20 @@ const REPOS = [repo('block/goose'), repo('block/builderbot')]; async function mountLabels( tagIds: string[], { + project = PROJECT, repos = REPOS, excludeTagGroupId = null, state = tagState(), - }: { repos?: ProjectRepo[]; excludeTagGroupId?: string | null; state?: TagState } = {} + }: { + project?: Project; + repos?: ProjectRepo[]; + excludeTagGroupId?: string | null; + state?: TagState; + } = {} ) { badges.clear(); badges.set('block/builderbot', { shortName: 'bb', hue: 200 }); + badges.set('block/builderbot:apps/staged', { shortName: 'staged', hue: 120 }); backend.state = { ...state, assignments: tagIds.map((tagId) => ({ projectId: 'p1', tagId })) }; await tagsStore.load({ force: true }); await tick(); @@ -81,7 +94,7 @@ async function mountLabels( document.body.appendChild(target); component = mount(ProjectCardLabels, { target, - props: { project: PROJECT, repos, excludeTagGroupId }, + props: { project, repos, excludeTagGroupId }, }); flushSync(); return target; @@ -92,6 +105,17 @@ function clusterTexts(root: ParentNode): string[] { return [...root.querySelector('.card-labels')!.children].map((el) => el.textContent!.trim()); } +/** The repos' labels, in order: what each shows and its tooltip. */ +function repoLabels(root: ParentNode): { text: string; title: string }[] { + return [ + ...root.querySelectorAll('.card-labels > .repo-badge-label, .repo-line'), + ].map((el) => ({ + text: el.textContent!.trim(), + // A badge carries the tooltip itself; a plain label's is its RepoLabel's. + title: el.title || el.querySelector('.repo-label')!.title, + })); +} + function chipNames(root: ParentNode): string[] { return [...root.querySelectorAll('.tag-chip-name')].map((el) => el.textContent!.trim()); } @@ -106,8 +130,9 @@ describe('ProjectCardLabels', () => { it('shows the tags as chips after the repos, in the same cluster', async () => { const target = await mountLabels(['infra', 'p0']); - // Repos sorted, then the chips: grouped tags, then ungrouped. - expect(clusterTexts(target)).toEqual(['block/builderbot', 'block/goose', 'P0', 'infra']); + // Repos sorted by full path (the badged one under its short name), then + // the chips: grouped tags, then ungrouped. + expect(clusterTexts(target)).toEqual(['bb', 'block/goose', 'P0', 'infra']); expect(target.querySelectorAll('.card-labels > .repo-badge-label')).toHaveLength(1); expect(target.querySelectorAll('.card-labels > .repo-line')).toHaveLength(1); expect(target.querySelectorAll('.card-labels > .tag-chip')).toHaveLength(2); @@ -116,11 +141,56 @@ describe('ProjectCardLabels', () => { it('adds nothing for a project without tags', async () => { const target = await mountLabels([]); - expect(clusterTexts(target)).toEqual(['block/builderbot', 'block/goose']); + expect(clusterTexts(target)).toEqual(['bb', 'block/goose']); expect(target.querySelector('.tag-chip')).toBeNull(); expect(target.querySelector('.tag-chips-more')).toBeNull(); }); + it('shows a badged repo’s short name, keeping the full path as its tooltip', async () => { + const target = await mountLabels([]); + + expect(repoLabels(target)).toEqual([ + { text: 'bb', title: 'block/builderbot' }, + // No badge, so no short name: the full path stays. + { text: 'block/goose', title: 'block/goose' }, + ]); + }); + + it('shows a monorepo subpath under its own short name', async () => { + const target = await mountLabels([], { + repos: [ + repo('block/builderbot', { subpath: 'apps/staged' }), + repo('block/builderbot'), + // A subpath without a badge keeps its full path. + repo('block/builderbot', { subpath: 'apps/web' }), + ], + }); + + // Still ordered by full path, whatever the short names are. + expect(repoLabels(target)).toEqual([ + { text: 'bb', title: 'block/builderbot' }, + { text: 'staged', title: 'block/builderbot/apps/staged' }, + { text: 'block/builderbot/apps/web', title: 'block/builderbot/apps/web' }, + ]); + }); + + it('names a fork-backed repo by its badge, with the fork’s path as the tooltip', async () => { + const target = await mountLabels([], { + repos: [repo('block/builderbot', { headRepo: 'someone/builderbot' })], + }); + + expect(repoLabels(target)).toEqual([{ text: 'bb', title: 'someone/builderbot' }]); + }); + + it('shortens the project’s own repo while its repos haven’t loaded', async () => { + const target = await mountLabels([], { + project: { ...PROJECT, githubRepo: 'block/builderbot', subpath: 'apps/staged' }, + repos: [], + }); + + expect(repoLabels(target)).toEqual([{ text: 'staged', title: 'block/builderbot/apps/staged' }]); + }); + it('shows the chips beside "No repo attached" for a project with no repo', async () => { const target = await mountLabels(['q4'], { repos: [] }); diff --git a/apps/staged/src/lib/features/projects/ProjectCardLabels.svelte b/apps/staged/src/lib/features/projects/ProjectCardLabels.svelte index 2643adbe8..c711ecc02 100644 --- a/apps/staged/src/lib/features/projects/ProjectCardLabels.svelte +++ b/apps/staged/src/lib/features/projects/ProjectCardLabels.svelte @@ -1,17 +1,20 @@