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
7 changes: 4 additions & 3 deletions src/components/ArchitectureFilters/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo, useState } from 'react';
import { useFilterToolbar } from '../shared/useFilterToolbar';
import FilterToolbar from '../shared/FilterToolbar';
import { compareLabels } from '../shared/compareLabels';

const DEFAULT_FILTERS = {
query: '',
Expand Down Expand Up @@ -59,9 +60,9 @@ export function useArchitectureFilterOptions(architectures) {
});
}
return {
industries: Array.from(industries).sort(),
projects: Array.from(projects).sort(),
organizations: Array.from(organizations).sort(),
industries: Array.from(industries).sort(compareLabels),
projects: Array.from(projects).sort(compareLabels),
organizations: Array.from(organizations).sort(compareLabels),
tags: Array.from(tags.values()).sort((a, b) =>
tagKey(a).localeCompare(tagKey(b)),
),
Expand Down
7 changes: 4 additions & 3 deletions src/components/CaseStudies/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import MirrorSyncStatus from '../shared/MirrorSyncStatus';
import { formatDate } from '../shared/formatDate';
import { useFilterToolbar } from '../shared/useFilterToolbar';
import FilterToolbar from '../shared/FilterToolbar';
import { compareLabels } from '../shared/compareLabels';
import styles from './styles.module.css';

const DEFAULT_FILTERS = { query: '', project: '', industry: '', country: '' };
Expand All @@ -19,9 +20,9 @@ function useCaseStudyFilterOptions(caseStudies) {
study.countries.forEach((c) => countries.add(c));
}
return {
projects: Array.from(projects).sort(),
industries: Array.from(industries).sort(),
countries: Array.from(countries).sort(),
projects: Array.from(projects).sort(compareLabels),
industries: Array.from(industries).sort(compareLabels),
countries: Array.from(countries).sort(compareLabels),
};
}, [caseStudies]);
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/MemberDirectory/hooks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { compareLabels } from '../shared/compareLabels';

export const MEMBERSHIP_FILTERS = [
{ value: 'member', label: 'End User Members' },
Expand All @@ -18,8 +19,8 @@ export function useFilterOptions(members) {
member.projects.forEach((p) => projects.add(p));
}
return {
industries: Array.from(industries).sort(),
projects: Array.from(projects).sort(),
industries: Array.from(industries).sort(compareLabels),
projects: Array.from(projects).sort(compareLabels),
membershipStatuses: MEMBERSHIP_FILTERS,
};
}, [members]);
Expand Down
15 changes: 15 additions & 0 deletions src/components/shared/compareLabels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Comparator for the filter option lists on the case-study,
* reference-architecture and member catalogs. Project and industry names are
* published in their own casing (`etcd`, `cert-manager`, `gaming`), and the
* default `.sort()` orders by code point, which lists every lowercase label
* after every capitalised one. Locale collation orders letters before case,
* and pinning the locale keeps the order the same on every runtime.
*
* @param {string} a
* @param {string} b
* @returns {number}
*/
export function compareLabels(a, b) {
return a.localeCompare(b, 'en');
}
31 changes: 31 additions & 0 deletions tests/architecture-filters-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,37 @@ test('option lists are de-duplicated and alphabetically sorted', () => {
]);
});

test('lowercase project, industry and organization names sort in alphabetical position', () => {
const { result } = renderHook(() =>
useArchitectureFilterOptions([
architecture({
organization: 'Zeiss',
industries: ['optics', 'Manufacturing'],
projects: ['Talos Linux', 'containerd', 'cert-manager'],
}),
architecture({
organization: 'adidas',
industries: ['gaming', 'Retail'],
projects: ['Cilium', 'stargz-snapshotter'],
}),
]),
);
assert.deepEqual(result.organizations, ['adidas', 'Zeiss']);
assert.deepEqual(result.industries, [
'gaming',
'Manufacturing',
'optics',
'Retail',
]);
assert.deepEqual(result.projects, [
'cert-manager',
'Cilium',
'containerd',
'stargz-snapshotter',
'Talos Linux',
]);
});

test('tags differing only in separators are listed once', () => {
const { result } = renderHook(() =>
useArchitectureFilterOptions([
Expand Down
39 changes: 38 additions & 1 deletion tests/case-studies.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,10 @@ function clearButton(view) {
);
}

// Project and industry names keep their published casing (etcd, gRPC), so the
// option order is collation order, not code-point order.
function uniqueSorted(values) {
return [...new Set(values)].sort();
return [...new Set(values)].sort((a, b) => a.localeCompare(b, 'en'));
}

test('the mirrored data set is non-empty, so these assertions are meaningful', () => {
Expand Down Expand Up @@ -505,6 +507,41 @@ async function renderWithData(fixture) {
}
}

test('filter options list lowercase project and industry names in alphabetical position', async () => {
const view = await renderWithData({
caseStudies: [
study({
id: 'a',
projects: ['Volcano', 'etcd', 'gRPC', 'Backstage'],
industries: ['Travel', 'iGaming', 'Transportation'],
countries: ['Canada', 'austria'],
}),
study({
id: 'b',
projects: ['cert-manager', 'Cilium'],
industries: ['Financial'],
countries: ['Canada'],
}),
],
});
const options = (id) => optionValues(elementById(view.tree, id));
assert.deepEqual(options('case-study-project'), [
'Backstage',
'cert-manager',
'Cilium',
'etcd',
'gRPC',
'Volcano',
]);
assert.deepEqual(options('case-study-industry'), [
'Financial',
'iGaming',
'Transportation',
'Travel',
]);
assert.deepEqual(options('case-study-country'), ['austria', 'Canada']);
});

test('a mirror with no case-studies list renders an empty table', async () => {
const view = await renderWithData({ generatedAt: '2026-01-02' });
assert.deepEqual(renderedTitles(view), []);
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/fixtures/accepted-regions.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"entries": [
{
"file": "src/components/CaseStudies/index.js",
"span": "52:38-52:44",
"span": "53:38-53:44",
"class": "data-shape-guard",
"reason": "`|| []` fallback taken only when data/case-studies.json has no caseStudies array."
},
Expand Down
41 changes: 37 additions & 4 deletions tests/member-directory.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -396,12 +396,15 @@ test('membership labels and filters distinguish every status', () => {
test('useFilterOptions returns the sorted distinct industries and projects', () => {
const { tree: result } = render(() => useFilterOptions(MEMBERS));

// Published casing is kept (cert-manager, containerd), so the expected
// order is collation order rather than code-point order.
const collate = (a, b) => a.localeCompare(b, 'en');
const expectedIndustries = [
...new Set(MEMBERS.flatMap((member) => member.industries)),
].sort();
].sort(collate);
const expectedProjects = [
...new Set(MEMBERS.flatMap((member) => member.projects)),
].sort();
].sort(collate);

assert.deepEqual(result.industries, expectedIndustries);
assert.deepEqual(result.projects, expectedProjects);
Expand All @@ -413,6 +416,35 @@ test('useFilterOptions returns the sorted distinct industries and projects', ()
);
});

test('useFilterOptions lists lowercase project and industry names in alphabetical position', () => {
const { tree: result } = render(() =>
useFilterOptions([
{
industries: ['Transport', 'gaming', 'Telecommunication'],
projects: ['Talos Linux', 'containerd', 'cert-manager', 'Cilium'],
},
{
industries: ['optics', 'Financial'],
projects: ['stargz-snapshotter', 'Cilium'],
},
]),
);
assert.deepEqual(result.industries, [
'Financial',
'gaming',
'optics',
'Telecommunication',
'Transport',
]);
assert.deepEqual(result.projects, [
'cert-manager',
'Cilium',
'containerd',
'stargz-snapshotter',
'Talos Linux',
]);
});

test('useFilterOptions recomputes its options when the member list changes', () => {
let input = [RICH_MEMBER];
const hook = renderHook(() => useFilterOptions(input));
Expand Down Expand Up @@ -621,12 +653,13 @@ test('each select names its facet and offers an "all" option first', () => {

test('the industry and project selects list every distinct value, sorted', () => {
const { view } = renderDirectory();
const collate = (a, b) => a.localeCompare(b, 'en');
const industries = [
...new Set(MEMBERS.flatMap((member) => member.industries)),
].sort();
].sort(collate);
const projects = [
...new Set(MEMBERS.flatMap((member) => member.projects)),
].sort();
].sort(collate);

assert.deepEqual(optionValues(view.tree, 'member-industry'), [
'',
Expand Down
5 changes: 4 additions & 1 deletion tests/reference-architectures.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,12 @@ test('the toolbar is told how many results of how many total are shown', () => {
'an unfiltered catalog shows every entry',
);
assert.equal(props.activeCount, 0);
// Collation order, so CERN follows Capital One rather than preceding it.
assert.deepEqual(
props.options.organizations,
[...new Set(catalog.map((a) => a.organization))].sort(),
[...new Set(catalog.map((a) => a.organization))].sort((a, b) =>
a.localeCompare(b, 'en'),
),
);
});

Expand Down
Loading