OmniVoice-Studio / frontend /src /components /settings /ApiKeysPanel.test.jsx
Lê Phi Nam
Deploy to HF Space
94004a2
Raw
History Blame Contribute Delete
6.23 kB
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
const STATE_THREE_UNSET = {
active: null,
sources: [
{ source: 'app', set: false, masked: null, whoami_user: null, whoami_ok: false },
{ source: 'env', set: false, masked: null, whoami_user: null, whoami_ok: false },
{ source: 'hf-cli', set: false, masked: null, whoami_user: null, whoami_ok: false },
],
};
const STATE_APP_ACTIVE = {
active: 'app',
sources: [
{ source: 'app', set: true, masked: 'hf_…abc', whoami_user: 'alice', whoami_ok: true },
{ source: 'env', set: false, masked: null, whoami_user: null, whoami_ok: false },
{ source: 'hf-cli', set: false, masked: null, whoami_user: null, whoami_ok: false },
],
};
const STATE_ENV_ACTIVE = {
active: 'env',
sources: [
{ source: 'app', set: false, masked: null, whoami_user: null, whoami_ok: false },
{ source: 'env', set: true, masked: 'hf_…xyz', whoami_user: 'bob', whoami_ok: true },
{ source: 'hf-cli', set: false, masked: null, whoami_user: null, whoami_ok: false },
],
};
function mockFetchOnce(payload, status = 200) {
return vi.fn().mockResolvedValueOnce({
ok: status >= 200 && status < 300,
status,
json: async () => payload,
text: async () => JSON.stringify(payload),
});
}
function mockFetchSequence(...responses) {
const fn = vi.fn();
for (const r of responses) {
fn.mockResolvedValueOnce({
ok: r.status >= 200 && r.status < 300,
status: r.status,
json: async () => r.body,
text: async () => JSON.stringify(r.body),
});
}
return fn;
}
import ApiKeysPanel from './ApiKeysPanel';
describe('ApiKeysPanel', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it('renders 3 source rows after mount', async () => {
global.fetch = mockFetchOnce(STATE_THREE_UNSET);
const { container } = render(<ApiKeysPanel />);
await waitFor(() => {
const rows = container.querySelectorAll('.apikeys-row');
expect(rows.length).toBe(3);
expect(container.querySelector('[data-source="app"]')).not.toBeNull();
expect(container.querySelector('[data-source="env"]')).not.toBeNull();
expect(container.querySelector('[data-source="hf-cli"]')).not.toBeNull();
});
});
it('shows the Active badge on the row matching state.active', async () => {
global.fetch = mockFetchOnce(STATE_APP_ACTIVE);
const { container } = render(<ApiKeysPanel />);
await waitFor(() => {
const appRow = container.querySelector('[data-source="app"]');
expect(appRow).not.toBeNull();
expect(appRow.classList.contains('apikeys-row--active')).toBe(true);
const badge = appRow.querySelector('.apikeys-badge--active');
expect(badge?.textContent).toMatch(/active/i);
});
});
it('moves the Active badge when the env source is active', async () => {
global.fetch = mockFetchOnce(STATE_ENV_ACTIVE);
const { container } = render(<ApiKeysPanel />);
await waitFor(() => {
const envRow = container.querySelector('[data-source="env"]');
expect(envRow?.classList.contains('apikeys-row--active')).toBe(true);
const appRow = container.querySelector('[data-source="app"]');
expect(appRow?.classList.contains('apikeys-row--active')).toBe(false);
});
});
it('Save button POSTs the entered token and refetches state', async () => {
const fetchMock = mockFetchSequence(
{ status: 200, body: STATE_THREE_UNSET }, // initial GET
{ status: 200, body: STATE_APP_ACTIVE }, // POST returns updated state
{ status: 200, body: STATE_APP_ACTIVE }, // GET after save
);
global.fetch = fetchMock;
render(<ApiKeysPanel />);
await waitFor(() => screen.getByPlaceholderText(/hf_/));
const input = screen.getByPlaceholderText(/hf_/);
fireEvent.change(input, { target: { value: 'hf_newtoken123' } });
const saveBtn = screen.getByRole('button', { name: /save/i });
fireEvent.click(saveBtn);
await waitFor(() => {
const calls = fetchMock.mock.calls;
// Find the POST call
const postCall = calls.find(([_url, opts]) => opts && opts.method === 'POST');
expect(postCall).toBeTruthy();
const [url, init] = postCall;
expect(url).toMatch(/\/api\/settings\/hf-token$/);
const body = JSON.parse(init.body);
expect(body).toEqual({ token: 'hf_newtoken123' });
});
});
it('Clear button shows confirmation dialog and DELETEs on confirm', async () => {
const fetchMock = mockFetchSequence(
{ status: 200, body: STATE_APP_ACTIVE }, // initial GET
{ status: 200, body: STATE_THREE_UNSET }, // DELETE response
{ status: 200, body: STATE_THREE_UNSET }, // refetch GET
);
global.fetch = fetchMock;
render(<ApiKeysPanel />);
await waitFor(() => screen.getByPlaceholderText(/hf_/));
const clearBtn = screen.getByRole('button', { name: /^clear$/i });
fireEvent.click(clearBtn);
// Dialog appears
expect(screen.getByText(/Clear the App-source HuggingFace token/)).toBeInTheDocument();
const confirmBtn = screen.getByRole('button', { name: /clear token/i });
fireEvent.click(confirmBtn);
await waitFor(() => {
const calls = fetchMock.mock.calls;
const del = calls.find(([_u, opts]) => opts && opts.method === 'DELETE');
expect(del).toBeTruthy();
expect(del[0]).toMatch(/\/api\/settings\/hf-token/);
// also_clear_hf_cli default is false → no query string
expect(del[0]).not.toMatch(/also_clear_hf_cli=true/);
});
});
it('"Test now" button refetches state', async () => {
const fetchMock = mockFetchSequence(
{ status: 200, body: STATE_THREE_UNSET },
{ status: 200, body: STATE_THREE_UNSET },
);
global.fetch = fetchMock;
render(<ApiKeysPanel />);
await waitFor(() => screen.getByPlaceholderText(/hf_/));
const testBtn = screen.getByRole('button', { name: /test now/i });
fireEvent.click(testBtn);
await waitFor(() => {
expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(2);
});
});
});