@gradio/core 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/dist/src/gradio_helper.js +2 -1
- package/dist/src/init.svelte.d.ts +1 -0
- package/dist/src/init.svelte.js +65 -8
- package/dist/src/init.test.skip.d.ts +1 -0
- package/dist/src/init.test.skip.js +627 -0
- package/package.json +50 -50
- package/src/gradio_helper.ts +4 -1
- package/src/i18n.test.ts +28 -2
- package/src/init.svelte.ts +76 -9
- package/src/{init.test.ts → init.test.skip.ts} +303 -4
package/package.json
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradio/core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"devDependencies": {
|
|
6
|
-
"@gradio/accordion": "^0.5.
|
|
7
|
-
"@gradio/annotatedimage": "^0.11.
|
|
8
|
-
"@gradio/
|
|
9
|
-
"@gradio/
|
|
10
|
-
"@gradio/box": "^0.2.
|
|
11
|
-
"@gradio/button": "^0.6.5",
|
|
6
|
+
"@gradio/accordion": "^0.5.34",
|
|
7
|
+
"@gradio/annotatedimage": "^0.11.6",
|
|
8
|
+
"@gradio/atoms": "^0.23.0",
|
|
9
|
+
"@gradio/audio": "^0.23.1",
|
|
10
|
+
"@gradio/box": "^0.2.31",
|
|
12
11
|
"@gradio/browserstate": "^0.3.7",
|
|
13
|
-
"@gradio/
|
|
14
|
-
"@gradio/checkbox": "^0.6.
|
|
15
|
-
"@gradio/
|
|
12
|
+
"@gradio/button": "^0.6.6",
|
|
13
|
+
"@gradio/checkbox": "^0.6.6",
|
|
14
|
+
"@gradio/chatbot": "^0.29.7",
|
|
15
|
+
"@gradio/checkboxgroup": "^0.10.1",
|
|
16
16
|
"@gradio/client": "^2.1.0",
|
|
17
|
-
"@gradio/code": "^0.17.
|
|
18
|
-
"@gradio/colorpicker": "^0.5.
|
|
17
|
+
"@gradio/code": "^0.17.6",
|
|
18
|
+
"@gradio/colorpicker": "^0.5.9",
|
|
19
19
|
"@gradio/column": "^0.3.2",
|
|
20
|
-
"@gradio/
|
|
21
|
-
"@gradio/
|
|
20
|
+
"@gradio/dataframe": "^0.23.0",
|
|
21
|
+
"@gradio/dataset": "^0.5.7",
|
|
22
22
|
"@gradio/downloadbutton": "^0.4.18",
|
|
23
|
-
"@gradio/
|
|
24
|
-
"@gradio/
|
|
25
|
-
"@gradio/
|
|
26
|
-
"@gradio/
|
|
27
|
-
"@gradio/fileexplorer": "^0.6.
|
|
28
|
-
"@gradio/
|
|
29
|
-
"@gradio/
|
|
30
|
-
"@gradio/group": "^0.3.
|
|
31
|
-
"@gradio/
|
|
32
|
-
"@gradio/
|
|
23
|
+
"@gradio/dropdown": "^0.11.7",
|
|
24
|
+
"@gradio/fallback": "^0.4.37",
|
|
25
|
+
"@gradio/datetime": "^0.4.6",
|
|
26
|
+
"@gradio/file": "^0.14.6",
|
|
27
|
+
"@gradio/fileexplorer": "^0.6.6",
|
|
28
|
+
"@gradio/form": "^0.3.2",
|
|
29
|
+
"@gradio/highlightedtext": "^0.11.5",
|
|
30
|
+
"@gradio/group": "^0.3.4",
|
|
31
|
+
"@gradio/gallery": "^0.17.5",
|
|
32
|
+
"@gradio/html": "^0.12.1",
|
|
33
33
|
"@gradio/icons": "^0.15.1",
|
|
34
|
-
"@gradio/image": "^0.
|
|
35
|
-
"@gradio/imageeditor": "^0.18.
|
|
36
|
-
"@gradio/imageslider": "^0.4.
|
|
37
|
-
"@gradio/
|
|
38
|
-
"@gradio/
|
|
39
|
-
"@gradio/
|
|
40
|
-
"@gradio/
|
|
41
|
-
"@gradio/
|
|
42
|
-
"@gradio/
|
|
43
|
-
"@gradio/
|
|
44
|
-
"@gradio/
|
|
45
|
-
"@gradio/plot": "^0.10.
|
|
46
|
-
"@gradio/
|
|
47
|
-
"@gradio/radio": "^0.9.4",
|
|
48
|
-
"@gradio/simpledropdown": "^0.3.35",
|
|
49
|
-
"@gradio/simpleimage": "^0.9.6",
|
|
50
|
-
"@gradio/simpletextbox": "^0.3.37",
|
|
34
|
+
"@gradio/image": "^0.26.1",
|
|
35
|
+
"@gradio/imageeditor": "^0.18.9",
|
|
36
|
+
"@gradio/imageslider": "^0.4.6",
|
|
37
|
+
"@gradio/label": "^0.6.6",
|
|
38
|
+
"@gradio/json": "^0.7.5",
|
|
39
|
+
"@gradio/model3d": "^0.16.7",
|
|
40
|
+
"@gradio/markdown": "^0.13.31",
|
|
41
|
+
"@gradio/multimodaltextbox": "^0.11.9",
|
|
42
|
+
"@gradio/number": "^0.8.6",
|
|
43
|
+
"@gradio/nativeplot": "^0.10.5",
|
|
44
|
+
"@gradio/paramviewer": "^0.9.7",
|
|
45
|
+
"@gradio/plot": "^0.10.7",
|
|
46
|
+
"@gradio/radio": "^0.10.1",
|
|
51
47
|
"@gradio/row": "^0.3.1",
|
|
52
|
-
"@gradio/
|
|
53
|
-
"@gradio/
|
|
48
|
+
"@gradio/simpledropdown": "^0.3.37",
|
|
49
|
+
"@gradio/sidebar": "^0.2.6",
|
|
50
|
+
"@gradio/simpleimage": "^0.9.8",
|
|
51
|
+
"@gradio/simpletextbox": "^0.3.39",
|
|
52
|
+
"@gradio/slider": "^0.7.9",
|
|
53
|
+
"@gradio/statustracker": "^0.13.1",
|
|
54
54
|
"@gradio/tabitem": "^0.6.6",
|
|
55
|
-
"@gradio/
|
|
56
|
-
"@gradio/
|
|
57
|
-
"@gradio/
|
|
55
|
+
"@gradio/state": "^0.2.3",
|
|
56
|
+
"@gradio/tabs": "^0.5.9",
|
|
57
|
+
"@gradio/textbox": "^0.13.7",
|
|
58
58
|
"@gradio/theme": "^0.6.1",
|
|
59
|
+
"@gradio/upload": "^0.17.8",
|
|
59
60
|
"@gradio/timer": "^0.4.9",
|
|
60
|
-
"@gradio/
|
|
61
|
-
"@gradio/utils": "^0.12.0",
|
|
61
|
+
"@gradio/utils": "^0.12.2",
|
|
62
62
|
"@gradio/uploadbutton": "^0.9.18",
|
|
63
|
-
"@gradio/
|
|
64
|
-
"@gradio/
|
|
63
|
+
"@gradio/vibeeditor": "^0.3.8",
|
|
64
|
+
"@gradio/video": "^0.20.6"
|
|
65
65
|
},
|
|
66
66
|
"msw": {
|
|
67
67
|
"workerDirectory": "public"
|
package/src/gradio_helper.ts
CHANGED
|
@@ -17,7 +17,10 @@ export function formatter(value: string | null | undefined): string {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const direct_translation = translate(string_value);
|
|
20
|
-
if (
|
|
20
|
+
if (
|
|
21
|
+
typeof direct_translation === "string" &&
|
|
22
|
+
direct_translation !== string_value
|
|
23
|
+
) {
|
|
21
24
|
return direct_translation;
|
|
22
25
|
}
|
|
23
26
|
|
package/src/i18n.test.ts
CHANGED
|
@@ -8,8 +8,31 @@ import {
|
|
|
8
8
|
afterEach
|
|
9
9
|
} from "vitest";
|
|
10
10
|
import { Lang, process_langs } from "./i18n";
|
|
11
|
-
|
|
11
|
+
// wikidata-lang/indexes/by_any_code uses Node.js createRequire internally.
|
|
12
|
+
// Import the raw JSON data and build the index directly for browser compatibility.
|
|
13
|
+
import languages from "wikidata-lang/data/languages.json";
|
|
12
14
|
import BCP47 from "./lang/BCP47_codes";
|
|
15
|
+
|
|
16
|
+
const languagesByAnyCode: Record<string, any[]> = {};
|
|
17
|
+
for (const langData of languages) {
|
|
18
|
+
for (const codeName of [
|
|
19
|
+
"wmCode",
|
|
20
|
+
"iso6391",
|
|
21
|
+
"iso6392",
|
|
22
|
+
"iso6393",
|
|
23
|
+
"iso6396"
|
|
24
|
+
]) {
|
|
25
|
+
const codes = (langData as Record<string, any>)[codeName];
|
|
26
|
+
if (!codes) continue;
|
|
27
|
+
for (const code of codes) {
|
|
28
|
+
if (languagesByAnyCode[code] == null) {
|
|
29
|
+
languagesByAnyCode[code] = [langData];
|
|
30
|
+
} else if (!languagesByAnyCode[code].includes(langData)) {
|
|
31
|
+
languagesByAnyCode[code].push(langData);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
13
36
|
import {
|
|
14
37
|
get_initial_locale,
|
|
15
38
|
load_translations,
|
|
@@ -25,7 +48,10 @@ vi.mock("svelte-i18n", () => ({
|
|
|
25
48
|
locale: { set: vi.fn() },
|
|
26
49
|
_: vi.fn((key) => `translated_${key}`),
|
|
27
50
|
addMessages: vi.fn(),
|
|
28
|
-
init: vi.fn().mockResolvedValue(undefined)
|
|
51
|
+
init: vi.fn().mockResolvedValue(undefined),
|
|
52
|
+
getLocaleFromNavigator: vi.fn(() => "en"),
|
|
53
|
+
register: vi.fn(),
|
|
54
|
+
waitLocale: vi.fn().mockResolvedValue(undefined)
|
|
29
55
|
}));
|
|
30
56
|
|
|
31
57
|
const mock_translations: Record<string, string> = {
|
package/src/init.svelte.ts
CHANGED
|
@@ -40,6 +40,20 @@ const type_map = {
|
|
|
40
40
|
walkthrough: "tabs",
|
|
41
41
|
walkthroughstep: "tabitem"
|
|
42
42
|
};
|
|
43
|
+
|
|
44
|
+
export function get_api_url(config: Omit<AppConfig, "api_url">): string {
|
|
45
|
+
// Handle api_prefix correctly when app is mounted at a subpath.
|
|
46
|
+
// config.root may not include a trailing slash, so we normalize its pathname
|
|
47
|
+
// before appending api_prefix to ensure correct URL construction.
|
|
48
|
+
const rootUrl = new URL(config.root);
|
|
49
|
+
const rootPath = rootUrl.pathname.endsWith("/")
|
|
50
|
+
? rootUrl.pathname
|
|
51
|
+
: rootUrl.pathname + "/";
|
|
52
|
+
const apiPrefix = config.api_prefix.startsWith("/")
|
|
53
|
+
? config.api_prefix
|
|
54
|
+
: "/" + config.api_prefix;
|
|
55
|
+
return new URL(rootPath.slice(0, -1) + apiPrefix, rootUrl.origin).toString();
|
|
56
|
+
}
|
|
43
57
|
export class AppTree {
|
|
44
58
|
/** the raw component structure received from the backend */
|
|
45
59
|
#component_payload: ComponentMeta[];
|
|
@@ -91,9 +105,10 @@ export class AppTree {
|
|
|
91
105
|
this.ready_resolve = resolve;
|
|
92
106
|
});
|
|
93
107
|
this.reactive_formatter = reactive_formatter;
|
|
108
|
+
const api_url = get_api_url(config);
|
|
94
109
|
this.#config = {
|
|
95
110
|
...config,
|
|
96
|
-
api_url
|
|
111
|
+
api_url
|
|
97
112
|
};
|
|
98
113
|
this.#component_payload = components;
|
|
99
114
|
this.#layout_payload = layout;
|
|
@@ -144,9 +159,10 @@ export class AppTree {
|
|
|
144
159
|
) {
|
|
145
160
|
this.#layout_payload = layout;
|
|
146
161
|
this.#component_payload = components;
|
|
162
|
+
const api_url = get_api_url(config);
|
|
147
163
|
this.#config = {
|
|
148
164
|
...config,
|
|
149
|
-
api_url
|
|
165
|
+
api_url
|
|
150
166
|
};
|
|
151
167
|
this.#dependency_payload = dependencies;
|
|
152
168
|
|
|
@@ -421,11 +437,6 @@ export class AppTree {
|
|
|
421
437
|
new_state: Partial<SharedProps> & Record<string, unknown>,
|
|
422
438
|
check_visibility: boolean = true
|
|
423
439
|
) {
|
|
424
|
-
// Visibility is tricky 😅
|
|
425
|
-
// If the component is not visible, it has not been rendered
|
|
426
|
-
// and so it has no _set_data callback
|
|
427
|
-
// Therefore, we need to traverse the tree and set the visible prop to true
|
|
428
|
-
// and then render it and its children. After that, we can call the _set_data callback
|
|
429
440
|
const node = find_node_by_id(this.root!, id);
|
|
430
441
|
let already_updated_visibility = false;
|
|
431
442
|
if (check_visibility && !node?.component) {
|
|
@@ -465,6 +476,12 @@ export class AppTree {
|
|
|
465
476
|
if ("value" in new_state && !dequal(old_value, new_state.value)) {
|
|
466
477
|
this.#event_dispatcher(id, "change", null);
|
|
467
478
|
}
|
|
479
|
+
|
|
480
|
+
// If this is a non-mounted tabitem, update the parent Tabs'
|
|
481
|
+
// initial_tabs so the tab button reflects the new state.
|
|
482
|
+
if (node?.type === "tabitem") {
|
|
483
|
+
this.#update_parent_tabs_initial_tab(id, node);
|
|
484
|
+
}
|
|
468
485
|
} else if (_set_data) {
|
|
469
486
|
_set_data(new_state);
|
|
470
487
|
}
|
|
@@ -492,6 +509,55 @@ export class AppTree {
|
|
|
492
509
|
});
|
|
493
510
|
}
|
|
494
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Updates the parent Tabs component's initial_tabs when a non-mounted
|
|
514
|
+
* tabitem's props change. This ensures the tab button (rendered by
|
|
515
|
+
* the Tabs component) reflects the updated state even though the
|
|
516
|
+
* TabItem component itself is not mounted.
|
|
517
|
+
*/
|
|
518
|
+
#update_parent_tabs_initial_tab(
|
|
519
|
+
id: number,
|
|
520
|
+
node: ProcessedComponentMeta
|
|
521
|
+
): void {
|
|
522
|
+
const parent = find_parent(this.root!, id);
|
|
523
|
+
if (!parent || parent.type !== "tabs") return;
|
|
524
|
+
|
|
525
|
+
const initial_tabs = parent.props.props.initial_tabs as Tab[];
|
|
526
|
+
if (!initial_tabs) return;
|
|
527
|
+
|
|
528
|
+
const tab_index = initial_tabs.findIndex((t) => t.component_id === node.id);
|
|
529
|
+
if (tab_index === -1) return;
|
|
530
|
+
|
|
531
|
+
const i18n = node.props.props.i18n as ((str: string) => string) | undefined;
|
|
532
|
+
const raw_label = node.props.shared_props.label as string;
|
|
533
|
+
// Use original_visibility since the node's visible may have been
|
|
534
|
+
// set to false by the startup optimization for non-selected tabs.
|
|
535
|
+
const visible =
|
|
536
|
+
"original_visibility" in node
|
|
537
|
+
? (node.original_visibility as boolean)
|
|
538
|
+
: (node.props.shared_props.visible as boolean);
|
|
539
|
+
initial_tabs[tab_index] = {
|
|
540
|
+
label: i18n ? i18n(raw_label) : raw_label,
|
|
541
|
+
id: node.props.props.id as string,
|
|
542
|
+
elem_id: node.props.shared_props.elem_id,
|
|
543
|
+
visible,
|
|
544
|
+
interactive: node.props.shared_props.interactive,
|
|
545
|
+
scale: node.props.shared_props.scale || null,
|
|
546
|
+
component_id: node.id
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// Trigger reactivity by replacing the array
|
|
550
|
+
parent.props.props.initial_tabs = [...initial_tabs];
|
|
551
|
+
|
|
552
|
+
// Also update via _set_data if the Tabs component is mounted
|
|
553
|
+
const parent_set_data = this.#set_callbacks.get(parent.id);
|
|
554
|
+
if (parent_set_data) {
|
|
555
|
+
parent_set_data({
|
|
556
|
+
initial_tabs: parent.props.props.initial_tabs
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
495
561
|
/**
|
|
496
562
|
* Gets the current state of a component by its ID
|
|
497
563
|
* @param id the ID of the component to get the state of
|
|
@@ -671,8 +737,9 @@ function gather_props(
|
|
|
671
737
|
|
|
672
738
|
_shared_props.load_component = (
|
|
673
739
|
name: string,
|
|
674
|
-
variant: "base" | "component" | "example"
|
|
675
|
-
|
|
740
|
+
variant: "base" | "component" | "example",
|
|
741
|
+
component_class_id?: string
|
|
742
|
+
) => get_component(name, component_class_id || "", api_url, variant);
|
|
676
743
|
|
|
677
744
|
_shared_props.visible =
|
|
678
745
|
_shared_props.visible === undefined ? true : _shared_props.visible;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect, vi } from "vitest";
|
|
2
2
|
import { spy } from "tinyspy";
|
|
3
|
-
import {
|
|
3
|
+
import { setupWorker } from "msw/browser";
|
|
4
4
|
import { http, HttpResponse } from "msw";
|
|
5
5
|
import type { client_return } from "@gradio/client";
|
|
6
6
|
import { Dependency, TargetMap } from "./types";
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
process_server_fn,
|
|
12
12
|
get_component
|
|
13
13
|
} from "./_init";
|
|
14
|
+
import { get_api_url } from "./init.svelte";
|
|
15
|
+
import type { AppConfig } from "./types";
|
|
16
|
+
import { commands } from "@vitest/browser/context";
|
|
14
17
|
|
|
15
18
|
describe("process_frontend_fn", () => {
|
|
16
19
|
test("empty source code returns null", () => {
|
|
@@ -461,6 +464,8 @@ describe("get_component", () => {
|
|
|
461
464
|
value: "hi",
|
|
462
465
|
interactive: false
|
|
463
466
|
},
|
|
467
|
+
key: "test-component-one",
|
|
468
|
+
|
|
464
469
|
has_modes: false,
|
|
465
470
|
instance: {} as any,
|
|
466
471
|
component: {} as any
|
|
@@ -512,13 +517,307 @@ describe("get_component", () => {
|
|
|
512
517
|
}
|
|
513
518
|
);
|
|
514
519
|
|
|
515
|
-
const
|
|
516
|
-
|
|
520
|
+
const worker = setupWorker(...handlers);
|
|
521
|
+
worker.start();
|
|
517
522
|
|
|
518
523
|
await get_component("test-random", id, api_url, []).component;
|
|
519
524
|
|
|
520
525
|
expect(mock).toHaveBeenCalled();
|
|
521
526
|
|
|
522
|
-
|
|
527
|
+
worker.stop();
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe("get_api_url", () => {
|
|
532
|
+
describe("root URL with trailing slash", () => {
|
|
533
|
+
test("root with trailing slash, api_prefix with leading slash", () => {
|
|
534
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
535
|
+
root: "http://example.com/myapp/",
|
|
536
|
+
api_prefix: "/api",
|
|
537
|
+
theme: "default",
|
|
538
|
+
version: "1.0.0",
|
|
539
|
+
autoscroll: true
|
|
540
|
+
};
|
|
541
|
+
const result = get_api_url(config);
|
|
542
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("root with trailing slash, api_prefix without leading slash", () => {
|
|
546
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
547
|
+
root: "http://example.com/myapp/",
|
|
548
|
+
api_prefix: "api",
|
|
549
|
+
theme: "default",
|
|
550
|
+
version: "1.0.0",
|
|
551
|
+
autoscroll: true
|
|
552
|
+
};
|
|
553
|
+
const result = get_api_url(config);
|
|
554
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("root at domain root with trailing slash", () => {
|
|
558
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
559
|
+
root: "http://example.com/",
|
|
560
|
+
api_prefix: "/api",
|
|
561
|
+
theme: "default",
|
|
562
|
+
version: "1.0.0",
|
|
563
|
+
autoscroll: true
|
|
564
|
+
};
|
|
565
|
+
const result = get_api_url(config);
|
|
566
|
+
expect(result).toBe("http://example.com/api");
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
describe("root URL without trailing slash", () => {
|
|
571
|
+
test("root without trailing slash, api_prefix with leading slash", () => {
|
|
572
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
573
|
+
root: "http://example.com/myapp",
|
|
574
|
+
api_prefix: "/api",
|
|
575
|
+
theme: "default",
|
|
576
|
+
version: "1.0.0",
|
|
577
|
+
autoscroll: true
|
|
578
|
+
};
|
|
579
|
+
const result = get_api_url(config);
|
|
580
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("root without trailing slash, api_prefix without leading slash", () => {
|
|
584
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
585
|
+
root: "http://example.com/myapp",
|
|
586
|
+
api_prefix: "api",
|
|
587
|
+
theme: "default",
|
|
588
|
+
version: "1.0.0",
|
|
589
|
+
autoscroll: true
|
|
590
|
+
};
|
|
591
|
+
const result = get_api_url(config);
|
|
592
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("root at domain root without trailing slash", () => {
|
|
596
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
597
|
+
root: "http://example.com",
|
|
598
|
+
api_prefix: "/api",
|
|
599
|
+
theme: "default",
|
|
600
|
+
version: "1.0.0",
|
|
601
|
+
autoscroll: true
|
|
602
|
+
};
|
|
603
|
+
const result = get_api_url(config);
|
|
604
|
+
expect(result).toBe("http://example.com/api");
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
describe("different root path combinations", () => {
|
|
609
|
+
test("root path is just '/'", () => {
|
|
610
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
611
|
+
root: "http://example.com/",
|
|
612
|
+
api_prefix: "/api",
|
|
613
|
+
theme: "default",
|
|
614
|
+
version: "1.0.0",
|
|
615
|
+
autoscroll: true
|
|
616
|
+
};
|
|
617
|
+
const result = get_api_url(config);
|
|
618
|
+
expect(result).toBe("http://example.com/api");
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("root path is '/' without trailing slash", () => {
|
|
622
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
623
|
+
root: "http://example.com",
|
|
624
|
+
api_prefix: "/api",
|
|
625
|
+
theme: "default",
|
|
626
|
+
version: "1.0.0",
|
|
627
|
+
autoscroll: true
|
|
628
|
+
};
|
|
629
|
+
const result = get_api_url(config);
|
|
630
|
+
expect(result).toBe("http://example.com/api");
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("root path is '/myapp'", () => {
|
|
634
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
635
|
+
root: "http://example.com/myapp",
|
|
636
|
+
api_prefix: "/api",
|
|
637
|
+
theme: "default",
|
|
638
|
+
version: "1.0.0",
|
|
639
|
+
autoscroll: true
|
|
640
|
+
};
|
|
641
|
+
const result = get_api_url(config);
|
|
642
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("root path is '/myapp/'", () => {
|
|
646
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
647
|
+
root: "http://example.com/myapp/",
|
|
648
|
+
api_prefix: "/api",
|
|
649
|
+
theme: "default",
|
|
650
|
+
version: "1.0.0",
|
|
651
|
+
autoscroll: true
|
|
652
|
+
};
|
|
653
|
+
const result = get_api_url(config);
|
|
654
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("root path is '/deep/nested/path'", () => {
|
|
658
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
659
|
+
root: "http://example.com/deep/nested/path",
|
|
660
|
+
api_prefix: "/api",
|
|
661
|
+
theme: "default",
|
|
662
|
+
version: "1.0.0",
|
|
663
|
+
autoscroll: true
|
|
664
|
+
};
|
|
665
|
+
const result = get_api_url(config);
|
|
666
|
+
expect(result).toBe("http://example.com/deep/nested/path/api");
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("root path is '/deep/nested/path/'", () => {
|
|
670
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
671
|
+
root: "http://example.com/deep/nested/path/",
|
|
672
|
+
api_prefix: "/api",
|
|
673
|
+
theme: "default",
|
|
674
|
+
version: "1.0.0",
|
|
675
|
+
autoscroll: true
|
|
676
|
+
};
|
|
677
|
+
const result = get_api_url(config);
|
|
678
|
+
expect(result).toBe("http://example.com/deep/nested/path/api");
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
describe("different api_prefix formats", () => {
|
|
683
|
+
test("api_prefix with leading slash", () => {
|
|
684
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
685
|
+
root: "http://example.com/myapp",
|
|
686
|
+
api_prefix: "/api",
|
|
687
|
+
theme: "default",
|
|
688
|
+
version: "1.0.0",
|
|
689
|
+
autoscroll: true
|
|
690
|
+
};
|
|
691
|
+
const result = get_api_url(config);
|
|
692
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
test("api_prefix without leading slash", () => {
|
|
696
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
697
|
+
root: "http://example.com/myapp",
|
|
698
|
+
api_prefix: "api",
|
|
699
|
+
theme: "default",
|
|
700
|
+
version: "1.0.0",
|
|
701
|
+
autoscroll: true
|
|
702
|
+
};
|
|
703
|
+
const result = get_api_url(config);
|
|
704
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("api_prefix with nested path and leading slash", () => {
|
|
708
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
709
|
+
root: "http://example.com/myapp",
|
|
710
|
+
api_prefix: "/api/v1",
|
|
711
|
+
theme: "default",
|
|
712
|
+
version: "1.0.0",
|
|
713
|
+
autoscroll: true
|
|
714
|
+
};
|
|
715
|
+
const result = get_api_url(config);
|
|
716
|
+
expect(result).toBe("http://example.com/myapp/api/v1");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("api_prefix with nested path without leading slash", () => {
|
|
720
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
721
|
+
root: "http://example.com/myapp",
|
|
722
|
+
api_prefix: "api/v1",
|
|
723
|
+
theme: "default",
|
|
724
|
+
version: "1.0.0",
|
|
725
|
+
autoscroll: true
|
|
726
|
+
};
|
|
727
|
+
const result = get_api_url(config);
|
|
728
|
+
expect(result).toBe("http://example.com/myapp/api/v1");
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
describe("edge cases", () => {
|
|
733
|
+
test("root with port number", () => {
|
|
734
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
735
|
+
root: "http://example.com:8080/myapp",
|
|
736
|
+
api_prefix: "/api",
|
|
737
|
+
theme: "default",
|
|
738
|
+
version: "1.0.0",
|
|
739
|
+
autoscroll: true
|
|
740
|
+
};
|
|
741
|
+
const result = get_api_url(config);
|
|
742
|
+
expect(result).toBe("http://example.com:8080/myapp/api");
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("root with HTTPS", () => {
|
|
746
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
747
|
+
root: "https://example.com/myapp",
|
|
748
|
+
api_prefix: "/api",
|
|
749
|
+
theme: "default",
|
|
750
|
+
version: "1.0.0",
|
|
751
|
+
autoscroll: true
|
|
752
|
+
};
|
|
753
|
+
const result = get_api_url(config);
|
|
754
|
+
expect(result).toBe("https://example.com/myapp/api");
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test("root with query parameters (should be ignored)", () => {
|
|
758
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
759
|
+
root: "http://example.com/myapp?param=value",
|
|
760
|
+
api_prefix: "/api",
|
|
761
|
+
theme: "default",
|
|
762
|
+
version: "1.0.0",
|
|
763
|
+
autoscroll: true
|
|
764
|
+
};
|
|
765
|
+
const result = get_api_url(config);
|
|
766
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
test("root with hash (should be ignored)", () => {
|
|
770
|
+
const config: Omit<AppConfig, "api_url"> = {
|
|
771
|
+
root: "http://example.com/myapp#section",
|
|
772
|
+
api_prefix: "/api",
|
|
773
|
+
theme: "default",
|
|
774
|
+
version: "1.0.0",
|
|
775
|
+
autoscroll: true
|
|
776
|
+
};
|
|
777
|
+
const result = get_api_url(config);
|
|
778
|
+
expect(result).toBe("http://example.com/myapp/api");
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
describe("consistency checks", () => {
|
|
783
|
+
test("same result regardless of root trailing slash", () => {
|
|
784
|
+
const baseConfig = {
|
|
785
|
+
api_prefix: "/api",
|
|
786
|
+
theme: "default",
|
|
787
|
+
version: "1.0.0",
|
|
788
|
+
autoscroll: true
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const config1: Omit<AppConfig, "api_url"> = {
|
|
792
|
+
...baseConfig,
|
|
793
|
+
root: "http://example.com/myapp"
|
|
794
|
+
};
|
|
795
|
+
const config2: Omit<AppConfig, "api_url"> = {
|
|
796
|
+
...baseConfig,
|
|
797
|
+
root: "http://example.com/myapp/"
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
expect(get_api_url(config1)).toBe(get_api_url(config2));
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test("same result regardless of api_prefix leading slash", () => {
|
|
804
|
+
const baseConfig = {
|
|
805
|
+
root: "http://example.com/myapp",
|
|
806
|
+
theme: "default",
|
|
807
|
+
version: "1.0.0",
|
|
808
|
+
autoscroll: true
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const config1: Omit<AppConfig, "api_url"> = {
|
|
812
|
+
...baseConfig,
|
|
813
|
+
api_prefix: "/api"
|
|
814
|
+
};
|
|
815
|
+
const config2: Omit<AppConfig, "api_url"> = {
|
|
816
|
+
...baseConfig,
|
|
817
|
+
api_prefix: "api"
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
expect(get_api_url(config1)).toBe(get_api_url(config2));
|
|
821
|
+
});
|
|
523
822
|
});
|
|
524
823
|
});
|