Files
webmail/lib/__tests__/settings-search.test.ts
T
ghoztyandClaude Opus 5.5 30f51e5ca4 feat: name the app with the configured APP_NAME in user-facing strings
Some user-facing strings still said "Bulwark" on a deployment with its own
APP_NAME: Settings > General > Default apps (protocol handlers), the "Open
Bulwark" notification, the rate-limit banner and toast, the plugin connect
dialog, one plugin permission text and the connector page.

These strings now take an {appName} placeholder, filled by productName():
the configured APP_NAME, or "Bulwark" when it is unset or the default
"Webmail" (the same rule as the TOTP issuer and the About card, #1160).
A stock install reads as before.

The settings search (the settings page and api.app.searchSettings) indexes
these texts with the name filled in, so "bulwark" or the configured name
finds Default apps and no result carries the raw placeholder.

Locales: the 13 keys in every locale that has "Bulwark" in them. Languages
that inflect the name (cs, et, hu, ko, ro, sk, tr) use a neutral form such
as "{appName} uygulamasında" instead of a declined "Bulwark'ta"; a native
check of those is welcome.

Not changed on purpose: the mobile app, push relay, Lite, demo and admin
texts, which speak about Bulwark itself.

Closes #1314

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 04:14:54 +03:00

157 lines
5.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import en from '@/locales/en/common.json';
import {
type SettingsSearchTab,
type SubResult,
collectSubResults,
flattenStrings,
getByPath,
tabSearchPaths,
withAppName,
} from '../settings-search';
function subResultsFor(tab: SettingsSearchTab): SubResult[] {
const list: SubResult[] = [];
for (const path of tabSearchPaths[tab]) {
collectSubResults(getByPath(en, path), list);
}
return list;
}
function haystackFor(tab: SettingsSearchTab): string {
const strings: string[] = [];
for (const path of tabSearchPaths[tab]) {
flattenStrings(getByPath(en, path), strings);
}
return strings.join(' ').toLowerCase();
}
function labelsFor(tab: SettingsSearchTab): string[] {
return subResultsFor(tab).map((r) => r.label);
}
describe('collectSubResults', () => {
it('emits flat `key` / `key_desc` string pairs as a labelled sub-result', () => {
const list: SubResult[] = [];
collectSubResults(
{
title: 'Calendar settings',
free_scroll: 'Free scrolling',
free_scroll_desc: 'Scroll continuously',
hover_preview_off: 'Disabled',
},
list
);
expect(list).toEqual([
{ label: 'Calendar settings', description: undefined },
{ label: 'Free scrolling', description: 'Scroll continuously' },
]);
});
it('still emits label/description objects and *_label keys', () => {
const list: SubResult[] = [];
collectSubResults(
{
name_label: 'Name',
nested: { label: 'Theme', description: 'Pick one', dark: 'Dark' },
},
list
);
expect(list).toEqual([
{ label: 'Name' },
{ label: 'Theme', description: 'Pick one' },
]);
});
});
describe('settings search index (English messages)', () => {
it('every tab path resolves to a translation subtree', () => {
const missing: string[] = [];
for (const paths of Object.values(tabSearchPaths)) {
for (const path of paths) {
if (getByPath(en, path) === undefined) missing.push(path);
}
}
expect(missing).toEqual([]);
});
it('lists the calendar toggles as sub-results', () => {
const labels = labelsFor('calendar');
expect(labels).toContain('Free scrolling');
expect(labels).toContain('Show week numbers');
expect(labels).toContain('Organize invitations as');
expect(labels).toContain('Parse email invitations');
expect(labelsFor('notifications')).toContain('Unread Count on Tab and App Icon');
expect(labelsFor('notifications')).not.toContain('Parse email invitations');
expect(labels).toContain('Contact birthday calendar');
// Option values are not settings of their own.
expect(labels).not.toContain('Disabled');
});
it('finds the newer mail settings on their tabs', () => {
expect(labelsFor('message_list')).toContain('Clear search when switching folders');
expect(labelsFor('message_list')).toContain('Swipe right action (mobile)');
expect(labelsFor('message_list')).toContain('Message list order');
expect(labelsFor('reading')).toContain('Trusted Senders');
expect(labelsFor('reading')).toContain('Email (.eml) filename');
expect(labelsFor('reading')).not.toContain('Delete Action');
expect(labelsFor('composing')).toContain('Undo send / send delay');
expect(labelsFor('composing')).toContain('Request read receipts by default');
expect(labelsFor('composing')).toContain('Email Templates');
expect(labelsFor('composing')).not.toContain('Sub-Address Delimiter');
expect(labelsFor('appearance')).toContain('Pro Interface (Experimental)');
expect(labelsFor('account')).toContain('Tell accounts apart');
expect(labelsFor('account')).toContain('Your Identities');
expect(labelsFor('appearance')).toContain('Themes');
expect(labelsFor('general')).toContain('Time zone');
expect(labelsFor('general')).toContain('Email links');
expect(labelsFor('organize')).toContain('Nested Tags');
expect(haystackFor('organize')).toContain('standard folder roles');
});
it('matches the calendar tab for a free-scroll query', () => {
expect(haystackFor('calendar')).toContain('free scrolling');
expect(haystackFor('general')).toContain('time zone');
});
});
describe('{appName} in the index (#1314)', () => {
const index = (appName: string) => {
const list: SubResult[] = [];
const strings: string[] = [];
for (const path of tabSearchPaths.general) {
const node = withAppName(getByPath(en, path), appName);
collectSubResults(node, list);
flattenStrings(node, strings);
}
return { list, haystack: strings.join(' ').toLowerCase() };
};
it('never keeps the raw placeholder', () => {
for (const tab of Object.keys(tabSearchPaths) as SettingsSearchTab[]) {
for (const path of tabSearchPaths[tab]) {
expect(JSON.stringify(withAppName(getByPath(en, path), 'Bulwark')) ?? '').not.toContain('{appName}');
}
}
});
it('finds Default apps by "bulwark" on a stock install', () => {
const { list, haystack } = index('Bulwark');
expect(haystack).toContain('bulwark');
expect(list.some((r) => r.description?.includes('Bulwark'))).toBe(true);
});
it('finds Default apps by the configured name', () => {
const { list, haystack } = index('Rath Mail');
expect(haystack).toContain('rath mail');
const links: string[] = [];
flattenStrings(withAppName(getByPath(en, 'protocol_handlers'), 'Rath Mail'), links);
expect(links.join(' ').toLowerCase()).not.toContain('bulwark');
expect(list.some((r) => r.description?.includes('Rath Mail'))).toBe(true);
});
it('leaves other values alone', () => {
expect(withAppName({ a: 'x {appName} y', b: ['{appName}'], c: 3 }, 'N')).toEqual({ a: 'x N y', b: ['N'], c: 3 });
});
});