+ {/* The four Google switches trade away quota that is the operator's on
+ a managed install; the search log below is the instance's own. */}
+ {!managed && (<>
}
/>
+ >)}
diff --git a/client/src/pages/AdminPage.test.tsx b/client/src/pages/AdminPage.test.tsx
index 1ee307532..3a02b2cba 100644
--- a/client/src/pages/AdminPage.test.tsx
+++ b/client/src/pages/AdminPage.test.tsx
@@ -1170,8 +1170,10 @@ describe('AdminPage', () => {
// By accessible name, not by position: the card gained an Amap field
// between Maps and Unsplash, and an index would have kept passing while
- // asserting about the wrong input.
- fireEvent.change(within(apiKeysCard!).getByLabelText('Unsplash API Key'), {
+ // asserting about the wrong input. The name sits on the show/hide toggle;
+ // the input is its sibling.
+ const unsplashToggle = within(apiKeysCard!).getByLabelText('Unsplash API Key');
+ fireEvent.change(unsplashToggle.parentElement!.querySelector('input')!, {
target: { value: 'test-unsplash-key' },
});
@@ -1198,9 +1200,10 @@ describe('AdminPage', () => {
fireEvent.click(screen.getByRole('button', { name: /settings/i }));
const apiKeysHeading = await screen.findByRole('heading', { name: /^api keys$/i });
- const apiKeysCard = apiKeysHeading.closest('.bg-white');
+ const apiKeysCard = apiKeysHeading.closest('.rounded-xl');
- fireEvent.change(within(apiKeysCard!).getByLabelText(/Amap/), {
+ const amapToggle = within(apiKeysCard!).getByLabelText(/Amap/);
+ fireEvent.change(amapToggle.parentElement!.querySelector('input')!, {
target: { value: 'test-amap-key' },
});
fireEvent.click(within(apiKeysCard!).getByRole('button', { name: /^save$/i }));
diff --git a/client/src/pages/admin/AdminSettingsTab.test.tsx b/client/src/pages/admin/AdminSettingsTab.test.tsx
index a1a430613..7195f731b 100644
--- a/client/src/pages/admin/AdminSettingsTab.test.tsx
+++ b/client/src/pages/admin/AdminSettingsTab.test.tsx
@@ -29,6 +29,15 @@ function card(heading: string | RegExp): HTMLElement {
return screen.getByRole('heading', { name: heading }).closest('.rounded-xl')!;
}
+/**
+ * The key input in the block whose show/hide toggle carries the given name. The
+ * toggle is the labelled element and the input its sibling, so this still finds
+ * the right field when the card gains a block, where an index would not.
+ */
+function keyInput(name: string | RegExp): HTMLInputElement {
+ return within(card('API Keys')).getByLabelText(name).parentElement!.querySelector('input')!;
+}
+
/** The toggle button in the row belonging to a label paragraph. */
function toggleFor(label: string): HTMLElement {
const row = screen.getAllByText(label).map(el => el.closest('.flex.items-center.justify-between'));
@@ -259,16 +268,15 @@ describe('AdminSettingsTab', () => {
it('FE-ADMSET-021: showKeys switches every key input to plain text', () => {
renderTab({ showKeys: { maps: true, unsplash: true, amap: true } });
- const keys = within(card('API Keys'));
- expect(keys.getByLabelText('Google Maps API Key')).toHaveAttribute('type', 'text');
- expect(keys.getByLabelText('Unsplash API Key')).toHaveAttribute('type', 'text');
- expect(keys.getByLabelText(/Amap/)).toHaveAttribute('type', 'text');
+ expect(keyInput('Google Maps API Key')).toHaveAttribute('type', 'text');
+ expect(keyInput('Unsplash API Key')).toHaveAttribute('type', 'text');
+ expect(keyInput(/Amap/)).toHaveAttribute('type', 'text');
});
it('FE-ADMSET-022: unsplash key input and its visibility toggle', () => {
const admin = renderTab({ unsplashKey: '' });
- const unsplashInput = within(card('API Keys')).getByLabelText('Unsplash API Key');
+ const unsplashInput = keyInput('Unsplash API Key');
fireEvent.change(unsplashInput, { target: { value: 'unsplash-key' } });
expect(admin.setUnsplashKey).toHaveBeenCalledWith('unsplash-key');
@@ -279,7 +287,7 @@ describe('AdminSettingsTab', () => {
it('FE-ADMSET-022b: amap key input and its visibility toggle', () => {
const admin = renderTab({ amapKey: '' });
- const amapInput = within(card('API Keys')).getByLabelText(/Amap/);
+ const amapInput = keyInput(/Amap/);
fireEvent.change(amapInput, { target: { value: 'amap-key' } });
expect(admin.setAmapKey).toHaveBeenCalledWith('amap-key');
@@ -298,12 +306,23 @@ describe('AdminSettingsTab', () => {
it('FE-ADMSET-022d: saving the provider goes through updateAppSettings, not the keys', () => {
const admin = renderTab({ placesProvider: 'auto' });
- fireEvent.change(within(card('API Keys')).getByLabelText('Place search provider'), {
- target: { value: 'amap' },
- });
+ // A CustomSelect: the trigger shows the current choice, the menu is portaled.
+ fireEvent.click(within(card('API Keys')).getByRole('button', { name: 'Automatic' }));
+ fireEvent.click(screen.getByRole('button', { name: 'Amap (高德地图)' }));
expect(admin.handleSavePlacesProvider).toHaveBeenCalledWith('amap');
});
+ it('FE-ADMSET-022e: a managed install hides the keys but keeps the provider choice', () => {
+ // The operator owns the credentials; which of them answers place search is
+ // still the admin's call (server managed.ts lists places_provider as theirs).
+ renderTab({ managed: true, placesProvider: 'auto' });
+
+ const keys = within(card('API Keys'));
+ expect(keys.queryByLabelText('Google Maps API Key')).not.toBeInTheDocument();
+ expect(keys.queryByLabelText(/Amap/)).not.toBeInTheDocument();
+ expect(keys.getByRole('button', { name: 'Automatic' })).toBeInTheDocument();
+ });
+
it('FE-ADMSET-023: the maps Test button is disabled without a key', () => {
renderTab({ mapsKey: '' });
diff --git a/client/src/pages/admin/AdminSettingsTab.tsx b/client/src/pages/admin/AdminSettingsTab.tsx
index 8b6739594..c93cf69a0 100644
--- a/client/src/pages/admin/AdminSettingsTab.tsx
+++ b/client/src/pages/admin/AdminSettingsTab.tsx
@@ -363,10 +363,10 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
- {/* Google and Unsplash come with the instance, and so does what a lookup costs;
- the per-place toggles only ever traded away quota that is not the customer’s
- to spend. Weather needs no key at all and has nothing to configure. */}
- {!managed && (<>
+ {/* The card itself stays on a managed install: the TREK index needs no key,
+ and which keyed provider answers place search is the admin's call there
+ too (server managed.ts). What folds away below is the operator's:
+ the keys, what a lookup costs, and the per-place Google switches. */}
{/* API Keys.
The key fields stay visible; only the four Google switches fold away.
They are set once and then never touched, and putting them flat next to
@@ -687,6 +687,7 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R
/>
+ {!managed && (
: }
{t('common.save')}
+ )}
- >)}
diff --git a/client/tests/helpers/mobileAdmin.ts b/client/tests/helpers/mobileAdmin.ts
index dec5313e0..40ab3e4c1 100644
--- a/client/tests/helpers/mobileAdmin.ts
+++ b/client/tests/helpers/mobileAdmin.ts
@@ -37,6 +37,7 @@ export function buildAdminHook(overrides: Record = {}): AdminHo
hour12: false,
mcpEnabled: false,
devMode: false,
+ managed: false,
currentUser: buildAdminUser(),
updateApiKeys: vi.fn(async () => undefined),
setAppRequireMfa: vi.fn(),
diff --git a/client/tests/unit/mobile/admin/MAdminSettingsSection.test.tsx b/client/tests/unit/mobile/admin/MAdminSettingsSection.test.tsx
index f143e3f02..b0813e9b3 100644
--- a/client/tests/unit/mobile/admin/MAdminSettingsSection.test.tsx
+++ b/client/tests/unit/mobile/admin/MAdminSettingsSection.test.tsx
@@ -266,6 +266,17 @@ describe('MAdminSettingsSection', () => {
expect(admin.handleSaveApiKeys).toHaveBeenCalled();
});
+ it('FE-MOB-ASET-016b: a managed install hides the keys but keeps the provider choice', () => {
+ // The operator owns the credentials; which of them answers place search is
+ // still the admin's call, the same split the desktop tab makes.
+ renderSettings({ managed: true });
+
+ expect(screen.queryByLabelText('Google Maps API Key')).not.toBeInTheDocument();
+ expect(screen.queryByLabelText(/Amap/)).not.toBeInTheDocument();
+ expect(screen.queryByLabelText('Unsplash API Key')).not.toBeInTheDocument();
+ expect(screen.getByDisplayValue('Automatic')).toBeInTheDocument();
+ });
+
it('FE-MOB-ASET-017: the Google Places toggles persist optimistically', async () => {
const user = userEvent.setup();
const seen: string[] = [];