Skip to content
Open
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
5 changes: 4 additions & 1 deletion packages/browser/src/FetchHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,12 @@ export class FetchHttpClient extends HttpClient {
method: (method ?? 'GET').toUpperCase(),
});
} catch (networkError: any) {
// A caller-aborted request rejects with an 'AbortError'; keep it distinguishable from a network failure.
const isAbort: boolean = (networkError as Error | undefined)?.name === 'AbortError';
throw Object.assign(new Error(networkError.message), {
code: 'NETWORK_ERROR',
code: isAbort ? 'ABORT_ERROR' : 'NETWORK_ERROR',
config,
name: isAbort ? 'AbortError' : 'Error',
} as Partial<HttpError>);
}

Expand Down
57 changes: 57 additions & 0 deletions packages/browser/src/__tests__/FetchHttpClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2026 The ThunderID Authors
// SPDX-License-Identifier: Apache-2.0

import type {HttpRequestConfig} from '@thunderid/javascript';
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import FetchHttpClient from '../FetchHttpClient';

/** `transport` is protected on purpose; these tests exercise it directly, below the request pipeline. */
const transport = (client: FetchHttpClient, config: HttpRequestConfig): Promise<unknown> =>
(client as unknown as {transport: (c: HttpRequestConfig) => Promise<unknown>}).transport(config);

describe('FetchHttpClient', () => {
beforeEach(() => {
FetchHttpClient.destroyInstance();
});

afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});

it('forwards a caller-supplied AbortSignal to the underlying fetch call', async () => {
const controller = new AbortController();
const fetchMock = vi.fn().mockResolvedValue({
headers: new Headers({'content-type': 'application/json'}),
json: () => Promise.resolve({ok: true}),
ok: true,
status: 200,
statusText: 'OK',
});
vi.stubGlobal('fetch', fetchMock);

const client: FetchHttpClient = FetchHttpClient.getInstance();
await transport(client, {method: 'GET', signal: controller.signal, url: 'https://localhost:8090/x'});

expect(fetchMock).toHaveBeenCalledWith(
'https://localhost:8090/x',
expect.objectContaining({signal: controller.signal}),
);
});

it('surfaces an aborted request as a distinguishable AbortError, not a generic NETWORK_ERROR', async () => {
const controller = new AbortController();
const abortError = new DOMException('The operation was aborted.', 'AbortError');
vi.stubGlobal(
'fetch',
vi.fn().mockImplementation(() => Promise.reject(abortError)),
);

const client: FetchHttpClient = FetchHttpClient.getInstance();
controller.abort();

await expect(
transport(client, {method: 'GET', signal: controller.signal, url: 'https://localhost:8090/x'}),
).rejects.toMatchObject({code: 'ABORT_ERROR', name: 'AbortError'});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,21 @@ describe('createHttpClientFetcher', () => {

expect(getInstanceSpy).toHaveBeenCalledWith(3);
});

it('forwards the caller-supplied AbortSignal so cancellation reaches the underlying request', async () => {
mockRequest.mockResolvedValueOnce({data: {ok: true}, status: 200, statusText: 'OK'});
const controller = new AbortController();

await createHttpClientFetcher()('https://localhost:8090/x', {method: 'POST', signal: controller.signal});

expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({signal: controller.signal}));
});

it('does not attach a signal when the caller did not supply one', async () => {
mockRequest.mockResolvedValueOnce({data: {ok: true}, status: 200, statusText: 'OK'});

await createHttpClientFetcher()('https://localhost:8090/x', {method: 'POST'});

expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({signal: undefined}));
});
});
1 change: 1 addition & 0 deletions packages/browser/src/utils/createHttpClientFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const createHttpClientFetcher = (instanceId = 0): ((url: string, config: Request
data: config.body ? JSON.parse(config.body as string) : undefined,
headers: config.headers as Record<string, string>,
method: config.method || 'POST',
signal: config.signal,
url,
} as HttpRequestConfig);

Expand Down
16 changes: 16 additions & 0 deletions packages/javascript/src/constants/UserSelectConstants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2026 The ThunderID Authors
// SPDX-License-Identifier: Apache-2.0

/**
* Defaults for turning directory users into USER_SELECT options.
*/
const UserSelectConstants: {
LABEL_ATTRIBUTES: readonly string[];
} = {
/**
* Fields tried, in order, for a user's visible label before falling back to the user's ID.
*/
LABEL_ATTRIBUTES: ['display', 'username', 'email'],
};

export default UserSelectConstants;
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/models/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ export interface I18nTranslations {
'elements.fields.organization.select.label': string;
'elements.fields.organization.select.placeholder': string;

/* Paged Select */
'elements.fields.paged_select.aria_label': string;
'elements.fields.paged_select.empty': string;
'elements.fields.paged_select.load_error': string;
'elements.fields.paged_select.load_more': string;
'elements.fields.paged_select.loaded': string;
'elements.fields.paged_select.loading': string;
'elements.fields.paged_select.loading_more': string;
'elements.fields.paged_select.retry': string;
'elements.fields.user_select.aria_label': string;
'elements.fields.user_select.empty': string;
'elements.fields.user_select.loading': string;

/* Validation */
'validations.required.field.error': string;
'validation.pattern.invalid': string;
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': 'Select Organization',
'elements.fields.organization.select.placeholder': 'Choose an organization',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'Select an option',
'elements.fields.paged_select.empty': 'No results found.',
'elements.fields.paged_select.load_error': 'Failed to load options.',
'elements.fields.paged_select.load_more': 'Load more',
'elements.fields.paged_select.loaded': '{count} loaded.',
'elements.fields.paged_select.loading': 'Loading…',
'elements.fields.paged_select.loading_more': 'Loading more…',
'elements.fields.paged_select.retry': 'Retry',
'elements.fields.user_select.aria_label': 'Select a user',
'elements.fields.user_select.empty': 'No users found.',
'elements.fields.user_select.loading': 'Loading users…',

/* Validation */
'validations.required.field.error': 'This field is required',
'validation.pattern.invalid': 'This value does not match the required format.',
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/fr-FR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': "Sélectionner l'organisation",
'elements.fields.organization.select.placeholder': 'Choisissez une organisation',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'Sélectionner une option',
'elements.fields.paged_select.empty': 'Aucun résultat trouvé.',
'elements.fields.paged_select.load_error': 'Échec du chargement des options.',
'elements.fields.paged_select.load_more': 'Charger plus',
'elements.fields.paged_select.loaded': '{count} chargé(s).',
'elements.fields.paged_select.loading': 'Chargement…',
'elements.fields.paged_select.loading_more': 'Chargement supplémentaire…',
'elements.fields.paged_select.retry': 'Réessayer',
'elements.fields.user_select.aria_label': 'Sélectionner un utilisateur',
'elements.fields.user_select.empty': 'Aucun utilisateur trouvé.',
'elements.fields.user_select.loading': 'Chargement des utilisateurs…',

/* Validation */
'validations.required.field.error': 'Ce champ est obligatoire',
'validation.pattern.invalid': 'Cette valeur ne correspond pas au format requis.',
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/hi-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': 'संगठन चुनें',
'elements.fields.organization.select.placeholder': 'एक संगठन चुनें',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'एक विकल्प चुनें',
'elements.fields.paged_select.empty': 'कोई परिणाम नहीं मिला।',
'elements.fields.paged_select.load_error': 'विकल्प लोड करने में विफल।',
'elements.fields.paged_select.load_more': 'और लोड करें',
'elements.fields.paged_select.loaded': '{count} लोड हुए।',
'elements.fields.paged_select.loading': 'लोड हो रहा है…',
'elements.fields.paged_select.loading_more': 'और लोड हो रहा है…',
'elements.fields.paged_select.retry': 'पुनः प्रयास करें',
'elements.fields.user_select.aria_label': 'एक उपयोगकर्ता चुनें',
'elements.fields.user_select.empty': 'कोई उपयोगकर्ता नहीं मिला।',
'elements.fields.user_select.loading': 'उपयोगकर्ता लोड हो रहे हैं…',

/* Validation */
'validations.required.field.error': 'यह फील्ड आवश्यक है',
'validation.pattern.invalid': 'यह मान आवश्यक प्रारूप से मेल नहीं खाता।',
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/ja-JP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': '組織を選択',
'elements.fields.organization.select.placeholder': '組織を選択してください',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'オプションを選択',
'elements.fields.paged_select.empty': '該当する結果がありません。',
'elements.fields.paged_select.load_error': 'オプションの読み込みに失敗しました。',
'elements.fields.paged_select.load_more': 'さらに読み込む',
'elements.fields.paged_select.loaded': '{count} 件読み込みました。',
'elements.fields.paged_select.loading': '読み込み中…',
'elements.fields.paged_select.loading_more': 'さらに読み込み中…',
'elements.fields.paged_select.retry': '再試行',
'elements.fields.user_select.aria_label': 'ユーザーを選択',
'elements.fields.user_select.empty': '該当するユーザーが見つかりません。',
'elements.fields.user_select.loading': 'ユーザーを読み込み中…',

/* Validation */
'validations.required.field.error': 'この項目は必須です',
'validation.pattern.invalid': 'この値は必要な形式と一致しません。',
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': 'Selecionar Organização',
'elements.fields.organization.select.placeholder': 'Selecione uma organização',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'Selecionar uma opção',
'elements.fields.paged_select.empty': 'Nenhum resultado encontrado.',
'elements.fields.paged_select.load_error': 'Falha ao carregar as opções.',
'elements.fields.paged_select.load_more': 'Carregar mais',
'elements.fields.paged_select.loaded': '{count} carregado(s).',
'elements.fields.paged_select.loading': 'Carregando…',
'elements.fields.paged_select.loading_more': 'Carregando mais…',
'elements.fields.paged_select.retry': 'Tentar novamente',
'elements.fields.user_select.aria_label': 'Selecionar um usuário',
'elements.fields.user_select.empty': 'Nenhum usuário encontrado.',
'elements.fields.user_select.loading': 'Carregando usuários…',

/* Validation */
'validations.required.field.error': 'Este campo é obrigatório',
'validation.pattern.invalid': 'Este valor não corresponde ao formato necessário.',
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/pt-PT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': 'Selecionar Organização',
'elements.fields.organization.select.placeholder': 'Selecione uma organização',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'Selecionar uma opção',
'elements.fields.paged_select.empty': 'Nenhum resultado encontrado.',
'elements.fields.paged_select.load_error': 'Falha ao carregar as opções.',
'elements.fields.paged_select.load_more': 'Carregar mais',
'elements.fields.paged_select.loaded': '{count} carregado(s).',
'elements.fields.paged_select.loading': 'A carregar…',
'elements.fields.paged_select.loading_more': 'A carregar mais…',
'elements.fields.paged_select.retry': 'Tentar novamente',
'elements.fields.user_select.aria_label': 'Selecionar um utilizador',
'elements.fields.user_select.empty': 'Nenhum utilizador encontrado.',
'elements.fields.user_select.loading': 'A carregar utilizadores…',

/* Validation */
'validations.required.field.error': 'Este campo é obrigatório',
'validation.pattern.invalid': 'Este valor não corresponde ao formato necessário.',
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/si-LK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': 'සංවිධානය තෝරන්න',
'elements.fields.organization.select.placeholder': 'සංවිධානයක් සැළුම් කරන්න',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'විකල්පයක් තෝරන්න',
'elements.fields.paged_select.empty': 'ප්‍රතිඵල හමු නොවීය.',
'elements.fields.paged_select.load_error': 'විකල්ප පූරණය කිරීම අසාර්ථක විය.',
'elements.fields.paged_select.load_more': 'තවත් පූරණය කරන්න',
'elements.fields.paged_select.loaded': '{count} පූරණය විය.',
'elements.fields.paged_select.loading': 'පූරණය වේ…',
'elements.fields.paged_select.loading_more': 'තවත් පූරණය වේ…',
'elements.fields.paged_select.retry': 'නැවත උත්සාහ කරන්න',
'elements.fields.user_select.aria_label': 'පරිශීලකයෙකු තෝරන්න',
'elements.fields.user_select.empty': 'පරිශීලකයන් හමු නොවීය.',
'elements.fields.user_select.loading': 'පරිශීලකයන් පූරණය වේ…',

/* Validation */
'validations.required.field.error': 'මෙම ක්ෂේත්‍රය අවශ්‍යයි',
'validation.pattern.invalid': 'මෙම අගය අවශ්‍ය ආකෘතියට නොගැලපේ.',
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/ta-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': 'அமைப்பை தேர்ந்தெடு',
'elements.fields.organization.select.placeholder': 'அமைப்பை தெரிந்தெடுக்கவும்',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'ஒரு விருப்பத்தைத் தேர்ந்தெடுக்கவும்',
'elements.fields.paged_select.empty': 'முடிவுகள் எதுவும் கிடைக்கவில்லை.',
'elements.fields.paged_select.load_error': 'விருப்பங்களை ஏற்ற முடியவில்லை.',
'elements.fields.paged_select.load_more': 'மேலும் ஏற்றவும்',
'elements.fields.paged_select.loaded': '{count} ஏற்றப்பட்டது.',
'elements.fields.paged_select.loading': 'ஏற்றுகிறது…',
'elements.fields.paged_select.loading_more': 'மேலும் ஏற்றுகிறது…',
'elements.fields.paged_select.retry': 'மீண்டும் முயற்சிக்கவும்',
'elements.fields.user_select.aria_label': 'ஒரு பயனரைத் தேர்ந்தெடுக்கவும்',
'elements.fields.user_select.empty': 'பயனர்கள் எவரும் கிடைக்கவில்லை.',
'elements.fields.user_select.loading': 'பயனர்களை ஏற்றுகிறது…',

/* Validation */
'validations.required.field.error': 'இந்த புலம் தேவை',
'validation.pattern.invalid': 'இந்த மதிப்பு தேவையான வடிவத்துடன் பொருந்தவில்லை.',
Expand Down
13 changes: 13 additions & 0 deletions packages/javascript/src/i18n/translations/te-IN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,19 @@ const translations: I18nTranslations = {
'elements.fields.organization.select.label': 'ఆర్గనైజేషన్‌ను ఎంచుకోండి',
'elements.fields.organization.select.placeholder': 'సంస్థను ఎంచుకోండి',

/* Paged Select */
'elements.fields.paged_select.aria_label': 'ఒక ఎంపికను ఎంచుకోండి',
'elements.fields.paged_select.empty': 'ఫలితాలు కనుగొనబడలేదు.',
'elements.fields.paged_select.load_error': 'ఎంపికలను లోడ్ చేయడంలో విఫలమైంది.',
'elements.fields.paged_select.load_more': 'మరిన్ని లోడ్ చేయండి',
'elements.fields.paged_select.loaded': '{count} లోడ్ అయ్యాయి.',
'elements.fields.paged_select.loading': 'లోడ్ అవుతోంది…',
'elements.fields.paged_select.loading_more': 'మరిన్ని లోడ్ అవుతోంది…',
'elements.fields.paged_select.retry': 'మళ్లీ ప్రయత్నించండి',
'elements.fields.user_select.aria_label': 'ఒక వినియోగదారుని ఎంచుకోండి',
'elements.fields.user_select.empty': 'వినియోగదారులు కనుగొనబడలేదు.',
'elements.fields.user_select.loading': 'వినియోగదారులను లోడ్ చేస్తోంది…',

/* Validation */
'validations.required.field.error': 'ఈ ఫీల్డ్ అవసరం',
'validation.pattern.invalid': 'ఈ విలువ అవసరమైన ఆకృతికి సరిపోలడం లేదు.',
Expand Down
Loading
Loading