@marimo-team/islands 0.15.5 → 0.16.1
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/dist/{ConnectedDataExplorerComponent-CBeIYi8p.js → ConnectedDataExplorerComponent-DyqLQGPc.js} +1567 -1544
- package/dist/{ImageComparisonComponent-Bk0a0xBq.js → ImageComparisonComponent-CQDGJfUA.js} +1 -1
- package/dist/{_baseUniq-utU5_Vu-.js → _baseUniq-B2Nna6Kt.js} +1 -1
- package/dist/{any-language-editor-PrUUh2lr.js → any-language-editor-D-wq0tOG.js} +1 -1
- package/dist/{architectureDiagram-W76B3OCA-D-vOp0UU.js → architectureDiagram-W76B3OCA-C6tdnMBf.js} +4 -4
- package/dist/assets/{worker-BcG8m3h5.js → worker-B0C57BK8.js} +40 -38
- package/dist/{blockDiagram-QIGZ2CNN-IG-z8q8A.js → blockDiagram-QIGZ2CNN-IagL8LCN.js} +5 -5
- package/dist/{c4Diagram-FPNF74CW-5AEXIX3t.js → c4Diagram-FPNF74CW-D3_lIWUP.js} +2 -2
- package/dist/{channel-ECVsTGGL.js → channel-DCJI_DKk.js} +1 -1
- package/dist/{chunk-4BX2VUAB-DfJcd9e-.js → chunk-4BX2VUAB-B2DrODwN.js} +1 -1
- package/dist/{chunk-55IACEB6-BwT8MejR.js → chunk-55IACEB6-BUWDsQ-t.js} +1 -1
- package/dist/{chunk-FMBD7UC4-DW7uxNR6.js → chunk-FMBD7UC4-BExPNFv1.js} +1 -1
- package/dist/{chunk-K7UQS3LO-BGn2ZPDQ.js → chunk-K7UQS3LO-Cixi-Yko.js} +4 -4
- package/dist/{chunk-QN33PNHL-BcIbOumv.js → chunk-QN33PNHL-B83MtvER.js} +1 -1
- package/dist/{chunk-QZHKN3VN-CMSnhk6x.js → chunk-QZHKN3VN-CXvbu85X.js} +1 -1
- package/dist/{chunk-TVAH2DTR-CZF2JRya.js → chunk-TVAH2DTR-CpiumCHg.js} +3 -3
- package/dist/{chunk-TZMSLE5B-BHzN_BY6.js → chunk-TZMSLE5B-DIzaZjcI.js} +1 -1
- package/dist/{classDiagram-v2-RKCZMP56-2H7MseyB.js → classDiagram-KNZD7YFC-DyN5HPdk.js} +2 -2
- package/dist/{classDiagram-KNZD7YFC-2H7MseyB.js → classDiagram-v2-RKCZMP56-DyN5HPdk.js} +2 -2
- package/dist/{clone-DKQcSK7N.js → clone-DrJYap2i.js} +1 -1
- package/dist/{cose-bilkent-S5V4N54A-CgvKFxTr.js → cose-bilkent-S5V4N54A-D39b4WrQ.js} +2 -2
- package/dist/{dagre-5GWH7T2D-VNFIipzt.js → dagre-5GWH7T2D-BLjRxDpS.js} +6 -6
- package/dist/{data-grid-overlay-editor-XdqkKCVx.js → data-grid-overlay-editor-DTALqerV.js} +2 -2
- package/dist/{diagram-N5W7TBWH-D1s8h-eH.js → diagram-N5W7TBWH-MM8AIKGR.js} +5 -5
- package/dist/{diagram-QEK2KX5R-DOa-AstT.js → diagram-QEK2KX5R-BZGarWuJ.js} +3 -3
- package/dist/{diagram-S2PKOQOG-CFZ-Y2zi.js → diagram-S2PKOQOG-CnPinN9Q.js} +3 -3
- package/dist/{dockerfile-zE-2DWBS.js → dockerfile-U8DnCJ4X.js} +1 -1
- package/dist/{erDiagram-AWTI2OKA-WxUYJfbS.js → erDiagram-AWTI2OKA-CvDVbxOO.js} +4 -4
- package/dist/{flowDiagram-PVAE7QVJ-dDZH2O1W.js → flowDiagram-PVAE7QVJ-C2uuBTZS.js} +5 -5
- package/dist/{ganttDiagram-OWAHRB6G-D3CCqPQq.js → ganttDiagram-OWAHRB6G-BEff10RF.js} +4 -4
- package/dist/{gitGraphDiagram-NY62KEGX-BHFylEwc.js → gitGraphDiagram-NY62KEGX-wggu0kb2.js} +4 -4
- package/dist/{glide-data-editor-D0aJSGV_.js → glide-data-editor-Bqh5_dzJ.js} +3 -3
- package/dist/{graph-BPGEu6c8.js → graph-DKpp_wzf.js} +3 -3
- package/dist/{index-HtOEKQ3O.js → index-4XruEJkp.js} +1 -1
- package/dist/{index-eDB61tLS.js → index-DW0BCGJE.js} +1 -1
- package/dist/{index-DotQhzoN.js → index-DdfF_cLK.js} +1 -1
- package/dist/{index-Bx2b23rX.js → index-DzJ_YPCG.js} +3 -3
- package/dist/{infoDiagram-STP46IZ2-DWhhqGPi.js → infoDiagram-STP46IZ2-DF7KW-Op.js} +2 -2
- package/dist/{journeyDiagram-BIP6EPQ6-CU8FpryL.js → journeyDiagram-BIP6EPQ6-B_jmhmqd.js} +3 -3
- package/dist/{kanban-definition-6OIFK2YF-CWhF_a4g.js → kanban-definition-6OIFK2YF-B-M9FTyw.js} +2 -2
- package/dist/{layout-DGonEvAZ.js → layout-C4oVYZZD.js} +4 -4
- package/dist/{linear-Cww2a6nQ.js → linear-C-HCGr0T.js} +1 -1
- package/dist/{main-Bc0LY9fB.js → main-B9x2-9f2.js} +93798 -93495
- package/dist/main.js +1 -1
- package/dist/{mermaid-DpJuOhRr.js → mermaid-BE4cM3Qs.js} +30 -30
- package/dist/{min-CFQjsG4L.js → min-DTpHJ698.js} +2 -2
- package/dist/{mindmap-definition-Q6HEUPPD-K513Ef1t.js → mindmap-definition-Q6HEUPPD-Cpd-hO1E.js} +3 -3
- package/dist/{number-overlay-editor-DuSchUfE.js → number-overlay-editor-CvURA2Ud.js} +2 -2
- package/dist/{pieDiagram-ADFJNKIX-DAIIUJJO.js → pieDiagram-ADFJNKIX-D9f_f6fn.js} +3 -3
- package/dist/{quadrantDiagram-LMRXKWRM-yuf-j7Os.js → quadrantDiagram-LMRXKWRM-DgllE7xw.js} +2 -2
- package/dist/{react-plotly-B378DZ9U.js → react-plotly-BU-JRJSi.js} +1 -1
- package/dist/{requirementDiagram-4UW4RH46-BBWvEl6q.js → requirementDiagram-4UW4RH46-Dk_G8eUb.js} +3 -3
- package/dist/{sankeyDiagram-GR3RE2ED-B_TwV-dS.js → sankeyDiagram-GR3RE2ED-BhLIhDc1.js} +1 -1
- package/dist/{sequenceDiagram-C3RYC4MD-BVC6lltp.js → sequenceDiagram-C3RYC4MD-DHoZdMFJ.js} +3 -3
- package/dist/{slides-component-CPX3S0Y9.js → slides-component-DXAgdf7K.js} +2 -2
- package/dist/{stateDiagram-KXAO66HF-BCU1tYTD.js → stateDiagram-KXAO66HF-C1Ie-7Xf.js} +4 -4
- package/dist/{stateDiagram-v2-UMBNRL4Z-BdvN6wTu.js → stateDiagram-v2-UMBNRL4Z--CRuIHtM.js} +2 -2
- package/dist/style.css +1 -1
- package/dist/{time-CSIip6fV.js → time-yQjlGPwa.js} +2 -2
- package/dist/{timeline-definition-XQNQX7LJ-CCxCPNQI.js → timeline-definition-XQNQX7LJ-D_PjxB1B.js} +1 -1
- package/dist/{treemap-75Q7IDZK-Du6v0BzD.js → treemap-75Q7IDZK--NYqQjUZ.js} +134 -134
- package/dist/{vega-component-Da93sTnp.js → vega-component-CCUOMM5K.js} +2 -2
- package/dist/{xychartDiagram-6GGTOJPD-Oq6xaZKR.js → xychartDiagram-6GGTOJPD-WLKsEnzs.js} +2 -2
- package/package.json +10 -5
- package/src/__tests__/mocks.ts +43 -0
- package/src/components/app-config/user-config-form.tsx +78 -1
- package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +116 -65
- package/src/components/chat/acp/__tests__/atoms.test.ts +1 -1
- package/src/components/chat/acp/__tests__/context-utils.test.ts +222 -0
- package/src/components/chat/acp/__tests__/prompt.test.ts +1 -1
- package/src/components/chat/acp/__tests__/state.test.ts +38 -42
- package/src/components/chat/acp/agent-docs.tsx +33 -6
- package/src/components/chat/acp/agent-panel.css +0 -18
- package/src/components/chat/acp/agent-panel.tsx +394 -72
- package/src/components/chat/acp/agent-selector.tsx +7 -1
- package/src/components/chat/acp/blocks.tsx +40 -10
- package/src/components/chat/acp/common.tsx +10 -2
- package/src/components/chat/acp/context-utils.ts +127 -0
- package/src/components/chat/acp/prompt.ts +96 -53
- package/src/components/chat/acp/state.ts +1 -1
- package/src/components/chat/acp/types.ts +8 -0
- package/src/components/chat/chat-panel.tsx +28 -89
- package/src/components/chat/chat-utils.ts +127 -1
- package/src/components/chat/markdown-renderer.css +39 -0
- package/src/components/chat/markdown-renderer.tsx +12 -47
- package/src/components/chat/tool-call-accordion.tsx +148 -26
- package/src/components/data-table/SearchBar.tsx +8 -7
- package/src/components/data-table/__tests__/column_formatting.test.ts +50 -35
- package/src/components/data-table/__tests__/data-table.test.tsx +39 -1
- package/src/components/data-table/cell-hover-template/feature.ts +14 -0
- package/src/components/data-table/cell-hover-template/types.ts +11 -0
- package/src/components/data-table/charts/components/form-fields.tsx +41 -37
- package/src/components/data-table/charts/forms/common-chart.tsx +2 -2
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +5 -2
- package/src/components/data-table/column-formatting/feature.ts +62 -29
- package/src/components/data-table/column-formatting/types.ts +1 -0
- package/src/components/data-table/column-header.tsx +3 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +24 -7
- package/src/components/data-table/column-summary/column-summary.tsx +18 -9
- package/src/components/data-table/columns.tsx +42 -18
- package/src/components/data-table/data-table.tsx +10 -2
- package/src/components/data-table/date-popover.tsx +85 -75
- package/src/components/data-table/filter-pills.tsx +14 -9
- package/src/components/data-table/header-items.tsx +5 -1
- package/src/components/data-table/pagination.tsx +20 -13
- package/src/components/data-table/renderers.tsx +28 -0
- package/src/components/data-table/row-viewer-panel/row-viewer.tsx +10 -8
- package/src/components/datasources/column-preview.tsx +6 -2
- package/src/components/datasources/datasources.tsx +8 -12
- package/src/components/editor/Cell.tsx +6 -0
- package/src/components/editor/actions/name-cell-input.tsx +6 -1
- package/src/components/editor/actions/useCellActionButton.tsx +3 -1
- package/src/components/editor/ai/__tests__/completion-utils.test.ts +178 -1
- package/src/components/editor/ai/add-cell-with-ai.tsx +68 -66
- package/src/components/editor/ai/ai-completion-editor.tsx +29 -26
- package/src/components/editor/ai/completion-handlers.tsx +44 -6
- package/src/components/editor/ai/completion-utils.ts +92 -0
- package/src/components/editor/ai/transport/chat-transport.tsx +39 -0
- package/src/components/editor/cell/CellStatus.tsx +23 -20
- package/src/components/editor/cell/CreateCellButton.tsx +3 -4
- package/src/components/editor/cell/StagedAICell.tsx +51 -0
- package/src/components/editor/cell/cell-actions.tsx +2 -1
- package/src/components/editor/cell/code/language-toggle.tsx +3 -4
- package/src/components/editor/chrome/wrapper/footer-items/machine-stats.tsx +39 -28
- package/src/components/editor/controls/notebook-menu-dropdown.tsx +4 -2
- package/src/components/editor/file-tree/requesting-tree.tsx +14 -8
- package/src/components/editor/renderers/CellArray.tsx +3 -4
- package/src/components/editor/renderers/slides-layout/slides-layout.tsx +3 -3
- package/src/components/editor/renderers/slides-layout/types.ts +1 -0
- package/src/components/pages/home-page.tsx +4 -1
- package/src/components/slides/slides-component.tsx +1 -1
- package/src/components/slides/slides.css +6 -0
- package/src/components/terminal/__tests__/state.test.ts +207 -0
- package/src/components/terminal/hooks.ts +41 -0
- package/src/components/terminal/state.ts +75 -0
- package/src/components/terminal/terminal.tsx +334 -13
- package/src/components/terminal/theme.tsx +57 -0
- package/src/components/tracing/tracing-spec.ts +5 -4
- package/src/components/ui/range-slider.tsx +4 -2
- package/src/components/ui/slider.tsx +3 -1
- package/src/components/variables/variables-table.tsx +3 -0
- package/src/core/MarimoApp.tsx +9 -6
- package/src/core/ai/__tests__/staged-cells.test.ts +356 -0
- package/src/core/ai/context/__tests__/registry.test.ts +6 -4
- package/src/core/ai/context/providers/cell-output.ts +3 -2
- package/src/core/ai/context/providers/error.ts +3 -1
- package/src/core/ai/context/providers/file.ts +7 -2
- package/src/core/ai/context/providers/tables.ts +3 -2
- package/src/core/ai/context/providers/variable.ts +6 -4
- package/src/core/ai/staged-cells.ts +241 -0
- package/src/core/cells/__tests__/add-missing-import.test.ts +67 -22
- package/src/core/cells/add-missing-import.ts +24 -7
- package/src/core/cells/cells.ts +27 -28
- package/src/core/cells/logs.ts +1 -1
- package/src/core/codemirror/find-replace/search-highlight.ts +3 -1
- package/src/core/codemirror/language/LanguageAdapters.ts +9 -3
- package/src/core/codemirror/lsp/federated-lsp.ts +1 -1
- package/src/core/codemirror/lsp/notebook-lsp.ts +8 -2
- package/src/core/codemirror/readonly/__tests__/extension.test.ts +1 -1
- package/src/core/codemirror/rtc/loro/awareness.ts +52 -17
- package/src/core/codemirror/rtc/loro/sync.ts +12 -4
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +4 -0
- package/src/core/hotkeys/hotkeys.ts +8 -4
- package/src/core/i18n/__tests__/locale-provider.test.tsx +176 -0
- package/src/core/i18n/locale-provider.tsx +35 -0
- package/src/core/i18n/with-locale.tsx +12 -0
- package/src/core/islands/components/web-components.tsx +13 -10
- package/src/core/islands/main.ts +2 -2
- package/src/core/kernel/RuntimeState.ts +4 -1
- package/src/core/kernel/messages.ts +8 -12
- package/src/core/network/DeferredRequestRegistry.ts +16 -4
- package/src/core/runtime/runtime.ts +5 -4
- package/src/core/saving/__tests__/filename.test.ts +37 -0
- package/src/core/static/__tests__/download-html.test.ts +43 -1
- package/src/core/wasm/bridge.ts +5 -1
- package/src/core/wasm/store.ts +4 -1
- package/src/core/wasm/worker/message-buffer.ts +3 -2
- package/src/core/websocket/types.ts +22 -16
- package/src/core/websocket/useMarimoWebSocket.tsx +2 -2
- package/src/css/app/Cell.css +11 -0
- package/src/hooks/useFormatting.ts +97 -0
- package/src/hooks/useTimer.ts +8 -5
- package/src/plugins/core/RenderHTML.tsx +36 -2
- package/src/plugins/core/__test__/RenderHTML.test.ts +72 -0
- package/src/plugins/core/registerReactComponent.tsx +44 -10
- package/src/plugins/impl/DataTablePlugin.tsx +4 -0
- package/src/plugins/impl/FileBrowserPlugin.tsx +8 -2
- package/src/plugins/impl/RangeSliderPlugin.tsx +5 -3
- package/src/plugins/impl/SliderPlugin.tsx +3 -1
- package/src/plugins/impl/anywidget/model.ts +16 -5
- package/src/plugins/impl/data-editor/types.ts +7 -5
- package/src/plugins/impl/data-explorer/components/column-summary.tsx +20 -13
- package/src/plugins/impl/panel/utils.ts +6 -4
- package/src/plugins/layout/OutlinePlugin.tsx +69 -0
- package/src/plugins/layout/StatPlugin.tsx +4 -1
- package/src/plugins/plugins.ts +2 -0
- package/src/stories/cell.stories.tsx +1 -1
- package/src/stories/layout/vertical/one-column.stories.tsx +1 -1
- package/src/utils/__tests__/cell-urls.test.ts +29 -0
- package/src/utils/__tests__/dates.test.ts +45 -24
- package/src/utils/__tests__/filenames.test.ts +18 -0
- package/src/utils/__tests__/numbers.test.ts +42 -30
- package/src/utils/__tests__/once.test.ts +187 -0
- package/src/utils/__tests__/path.test.ts +38 -0
- package/src/utils/__tests__/urls.test.ts +56 -1
- package/src/utils/dates.ts +15 -10
- package/src/utils/edit-distance.ts +8 -6
- package/src/utils/errors.ts +9 -0
- package/src/utils/id-tree.tsx +21 -10
- package/src/utils/localStorage.ts +13 -4
- package/src/utils/numbers.ts +11 -11
- package/src/utils/once.ts +32 -0
- package/src/utils/paths.ts +4 -1
- package/src/utils/pluralize.ts +12 -5
- package/src/utils/python-poet/poet.ts +30 -15
- package/src/utils/time.ts +5 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
import { Provider, useAtomValue } from "jotai";
|
|
3
|
+
import type { JSX } from "react";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { notebookOutline } from "@/core/cells/cells";
|
|
6
|
+
import { store } from "@/core/state/jotai";
|
|
7
|
+
import { OutlineList } from "../../components/editor/chrome/panels/outline/floating-outline";
|
|
8
|
+
import {
|
|
9
|
+
findOutlineElements,
|
|
10
|
+
useActiveOutline,
|
|
11
|
+
} from "../../components/editor/chrome/panels/outline/useActiveOutline";
|
|
12
|
+
import type {
|
|
13
|
+
IStatelessPlugin,
|
|
14
|
+
IStatelessPluginProps,
|
|
15
|
+
} from "../stateless-plugin";
|
|
16
|
+
|
|
17
|
+
interface Data {
|
|
18
|
+
label?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const OutlineContent: React.FC<{ label?: string }> = ({ label }) => {
|
|
22
|
+
const { items } = useAtomValue(notebookOutline);
|
|
23
|
+
const headerElements = findOutlineElements(items);
|
|
24
|
+
const { activeHeaderId, activeOccurrences } =
|
|
25
|
+
useActiveOutline(headerElements);
|
|
26
|
+
|
|
27
|
+
if (items.length === 0) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="text-muted-foreground text-sm p-4 border border-dashed border-border rounded-lg">
|
|
30
|
+
No outline found. Add markdown headings to your notebook to create an
|
|
31
|
+
outline.
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="border border-border rounded-lg">
|
|
38
|
+
{label && (
|
|
39
|
+
<div className="px-4 py-2 border-b border-border font-medium text-sm">
|
|
40
|
+
{label}
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
<OutlineList
|
|
44
|
+
className="max-h-[400px]"
|
|
45
|
+
items={items}
|
|
46
|
+
activeHeaderId={activeHeaderId}
|
|
47
|
+
activeOccurrences={activeOccurrences}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export class OutlinePlugin implements IStatelessPlugin<Data> {
|
|
54
|
+
tagName = "marimo-outline";
|
|
55
|
+
|
|
56
|
+
validator = z.object({
|
|
57
|
+
label: z.string().optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
render(props: IStatelessPluginProps<Data>): JSX.Element {
|
|
61
|
+
const { label } = props.data;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Provider store={store}>
|
|
65
|
+
<OutlineContent label={label} />
|
|
66
|
+
</Provider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { TriangleIcon } from "lucide-react";
|
|
4
4
|
import type { JSX } from "react";
|
|
5
|
+
import { useLocale } from "react-aria";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { cn } from "@/utils/cn";
|
|
7
8
|
import { prettyNumber } from "@/utils/numbers";
|
|
@@ -41,6 +42,8 @@ export const StatComponent: React.FC<Data> = ({
|
|
|
41
42
|
bordered,
|
|
42
43
|
direction,
|
|
43
44
|
}) => {
|
|
45
|
+
const { locale } = useLocale();
|
|
46
|
+
|
|
44
47
|
const renderPrettyValue = () => {
|
|
45
48
|
if (value == null) {
|
|
46
49
|
return <i>No value</i>;
|
|
@@ -51,7 +54,7 @@ export const StatComponent: React.FC<Data> = ({
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
if (typeof value === "number") {
|
|
54
|
-
return prettyNumber(value);
|
|
57
|
+
return prettyNumber(value, locale);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
if (typeof value === "boolean") {
|
package/src/plugins/plugins.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { JsonOutputPlugin } from "./layout/JsonOutputPlugin";
|
|
|
45
45
|
import { LazyPlugin } from "./layout/LazyPlugin";
|
|
46
46
|
import { MimeRendererPlugin } from "./layout/MimeRenderPlugin";
|
|
47
47
|
import { MermaidPlugin } from "./layout/mermaid/MermaidPlugin";
|
|
48
|
+
import { OutlinePlugin } from "./layout/OutlinePlugin";
|
|
48
49
|
import { ProgressPlugin } from "./layout/ProgressPlugin";
|
|
49
50
|
import { RoutesPlugin } from "./layout/RoutesPlugin";
|
|
50
51
|
import { StatPlugin } from "./layout/StatPlugin";
|
|
@@ -99,6 +100,7 @@ const LAYOUT_PLUGINS: Array<IStatelessPlugin<unknown>> = [
|
|
|
99
100
|
new MimeRendererPlugin(),
|
|
100
101
|
new MermaidPlugin(),
|
|
101
102
|
new NavigationMenuPlugin(),
|
|
103
|
+
new OutlinePlugin(),
|
|
102
104
|
new ProgressPlugin(),
|
|
103
105
|
new RoutesPlugin(),
|
|
104
106
|
new StatPlugin(),
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { defaultUserConfig } from "@/core/config/config-schema";
|
|
12
12
|
import { connectionAtom } from "@/core/network/connection";
|
|
13
13
|
import { requestClientAtom } from "@/core/network/requests";
|
|
14
|
-
import { resolveRequestClient } from "@/core/network/resolve
|
|
14
|
+
import { resolveRequestClient } from "@/core/network/resolve";
|
|
15
15
|
import type { CellConfig } from "@/core/network/types";
|
|
16
16
|
import { WebSocketState } from "@/core/websocket/types";
|
|
17
17
|
import { MultiColumn } from "@/utils/id-tree";
|
|
@@ -9,7 +9,7 @@ import { defaultUserConfig, parseAppConfig } from "@/core/config/config-schema";
|
|
|
9
9
|
import { showCodeInRunModeAtom } from "@/core/meta/state";
|
|
10
10
|
import { connectionAtom } from "@/core/network/connection";
|
|
11
11
|
import { requestClientAtom } from "@/core/network/requests";
|
|
12
|
-
import { resolveRequestClient } from "@/core/network/resolve
|
|
12
|
+
import { resolveRequestClient } from "@/core/network/resolve";
|
|
13
13
|
import { WebSocketState } from "@/core/websocket/types";
|
|
14
14
|
import { MultiColumn } from "@/utils/id-tree";
|
|
15
15
|
import type { Milliseconds, Seconds } from "@/utils/time";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { EDGE_CASE_CELL_NAMES } from "../../__tests__/mocks";
|
|
3
4
|
import {
|
|
4
5
|
canLinkToCell,
|
|
5
6
|
createCellLink,
|
|
@@ -89,4 +90,32 @@ describe("cell-urls utilities", () => {
|
|
|
89
90
|
expect(canLinkToCell("_")).toBe(false);
|
|
90
91
|
});
|
|
91
92
|
});
|
|
93
|
+
|
|
94
|
+
describe("edge case cell names with unicode and special characters", () => {
|
|
95
|
+
it.each(EDGE_CASE_CELL_NAMES)(
|
|
96
|
+
"should handle unicode cell names in createCellLink: %s",
|
|
97
|
+
(cellName) => {
|
|
98
|
+
const url = createCellLink(cellName);
|
|
99
|
+
expect(url).toContain("scrollTo=");
|
|
100
|
+
expect(url).toContain(encodeURIComponent(cellName));
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
it.each(EDGE_CASE_CELL_NAMES)(
|
|
105
|
+
"should round-trip unicode cell names correctly: %s",
|
|
106
|
+
(cellName) => {
|
|
107
|
+
const url = createCellLink(cellName);
|
|
108
|
+
const hash = url.split("#")[1];
|
|
109
|
+
const extracted = extractCellNameFromHash(`#${hash}`);
|
|
110
|
+
expect(extracted).toBe(cellName);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
it.each(EDGE_CASE_CELL_NAMES)(
|
|
115
|
+
"should allow linking to unicode cell names: %s",
|
|
116
|
+
(cellName) => {
|
|
117
|
+
expect(canLinkToCell(cellName)).toBe(true);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
});
|
|
92
121
|
});
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
timeAgo,
|
|
9
9
|
} from "../dates";
|
|
10
10
|
|
|
11
|
+
const locale = "en-US";
|
|
12
|
+
|
|
11
13
|
describe("dates", () => {
|
|
12
14
|
// Save original timezone
|
|
13
15
|
let originalTimezone: string | undefined;
|
|
@@ -25,36 +27,38 @@ describe("dates", () => {
|
|
|
25
27
|
|
|
26
28
|
describe("prettyDate", () => {
|
|
27
29
|
it("returns empty string for null or undefined", () => {
|
|
28
|
-
expect(prettyDate(null, "date")).toBe("");
|
|
29
|
-
expect(prettyDate(undefined, "date")).toBe("");
|
|
30
|
+
expect(prettyDate(null, "date", locale)).toBe("");
|
|
31
|
+
expect(prettyDate(undefined, "date", locale)).toBe("");
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
it("formats date correctly", () => {
|
|
33
35
|
const date = new Date("2023-05-15T12:00:00Z");
|
|
34
36
|
// Using a regex to match the pattern since exact format may vary by locale
|
|
35
|
-
expect(prettyDate(date.toISOString(), "date")).toMatch(
|
|
37
|
+
expect(prettyDate(date.toISOString(), "date", locale)).toMatch(
|
|
38
|
+
/May 15, 2023/,
|
|
39
|
+
);
|
|
36
40
|
});
|
|
37
41
|
|
|
38
42
|
it("formats datetime correctly", () => {
|
|
39
43
|
const date = new Date("2023-05-15T12:00:00Z");
|
|
40
|
-
expect(prettyDate(date.toISOString(), "datetime")).toMatch(
|
|
44
|
+
expect(prettyDate(date.toISOString(), "datetime", locale)).toMatch(
|
|
41
45
|
/May 15, 2023/,
|
|
42
46
|
);
|
|
43
47
|
});
|
|
44
48
|
|
|
45
49
|
it("handles errors gracefully", () => {
|
|
46
|
-
expect(prettyDate("invalid-date", "date")).toBe("Invalid Date");
|
|
50
|
+
expect(prettyDate("invalid-date", "date", locale)).toBe("Invalid Date");
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
it("handles numeric timestamp input", () => {
|
|
50
54
|
const timestamp = 1_684_152_000_000; // 2023-05-15T12:00:00Z in milliseconds
|
|
51
|
-
expect(prettyDate(timestamp, "date")).toMatch(/May 15, 2023/);
|
|
55
|
+
expect(prettyDate(timestamp, "date", locale)).toMatch(/May 15, 2023/);
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
it("preserves timezone for datetime type", () => {
|
|
55
59
|
// This date is in winter time to avoid daylight saving time issues
|
|
56
60
|
const date = new Date("2023-01-15T15:30:00Z");
|
|
57
|
-
expect(prettyDate(date.toISOString(), "datetime")).toMatch(
|
|
61
|
+
expect(prettyDate(date.toISOString(), "datetime", locale)).toMatch(
|
|
58
62
|
/Jan 15, 2023/,
|
|
59
63
|
);
|
|
60
64
|
});
|
|
@@ -62,7 +66,9 @@ describe("dates", () => {
|
|
|
62
66
|
it("drops timezone for date type by using UTC", () => {
|
|
63
67
|
// Create a date that would be different days in different timezones
|
|
64
68
|
const date = new Date("2023-05-15T23:30:00Z"); // Late in the day UTC
|
|
65
|
-
expect(prettyDate(date.toISOString(), "date")).toMatch(
|
|
69
|
+
expect(prettyDate(date.toISOString(), "date", locale)).toMatch(
|
|
70
|
+
/May 15, 2023/,
|
|
71
|
+
);
|
|
66
72
|
});
|
|
67
73
|
|
|
68
74
|
describe("with different locales", () => {
|
|
@@ -86,7 +92,9 @@ describe("dates", () => {
|
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
const date = new Date("2023-05-15T12:00:00Z");
|
|
89
|
-
expect(prettyDate(date.toISOString(), "date")).toBe(
|
|
95
|
+
expect(prettyDate(date.toISOString(), "date", locale)).toBe(
|
|
96
|
+
"15 mai 2023",
|
|
97
|
+
);
|
|
90
98
|
});
|
|
91
99
|
});
|
|
92
100
|
});
|
|
@@ -94,34 +102,42 @@ describe("dates", () => {
|
|
|
94
102
|
describe("exactDateTime", () => {
|
|
95
103
|
it("formats date without milliseconds", () => {
|
|
96
104
|
const date = new Date("2023-05-15T12:00:00.000Z");
|
|
97
|
-
expect(exactDateTime(date, undefined)).toBe(
|
|
105
|
+
expect(exactDateTime(date, undefined, locale)).toBe(
|
|
106
|
+
"2023-05-15 12:00:00",
|
|
107
|
+
);
|
|
98
108
|
});
|
|
99
109
|
|
|
100
110
|
it("formats date with milliseconds", () => {
|
|
101
111
|
const date = new Date("2023-05-15T12:00:00.123Z");
|
|
102
|
-
expect(exactDateTime(date, undefined)).toBe(
|
|
112
|
+
expect(exactDateTime(date, undefined, locale)).toBe(
|
|
113
|
+
"2023-05-15 12:00:00.123",
|
|
114
|
+
);
|
|
103
115
|
});
|
|
104
116
|
|
|
105
117
|
it("formats date in UTC when renderInUTC is true", () => {
|
|
106
118
|
const date = new Date("2023-05-15T12:00:00.000Z");
|
|
107
|
-
expect(exactDateTime(date, "UTC")).toBe(
|
|
119
|
+
expect(exactDateTime(date, "UTC", locale)).toBe(
|
|
120
|
+
"2023-05-15 12:00:00 UTC",
|
|
121
|
+
);
|
|
108
122
|
});
|
|
109
123
|
|
|
110
124
|
it("formats date with milliseconds in UTC when renderInUTC is true", () => {
|
|
111
125
|
const date = new Date("2023-05-15T12:00:00.123Z");
|
|
112
|
-
expect(exactDateTime(date, "UTC")).toBe(
|
|
126
|
+
expect(exactDateTime(date, "UTC", locale)).toBe(
|
|
127
|
+
"2023-05-15 12:00:00.123 UTC",
|
|
128
|
+
);
|
|
113
129
|
});
|
|
114
130
|
|
|
115
131
|
it("formats date in America/New_York timezone", () => {
|
|
116
132
|
const date = new Date("2023-05-15T12:00:00.000Z");
|
|
117
|
-
expect(exactDateTime(date, "America/New_York")).toBe(
|
|
133
|
+
expect(exactDateTime(date, "America/New_York", locale)).toBe(
|
|
118
134
|
"2023-05-15 08:00:00 EDT",
|
|
119
135
|
);
|
|
120
136
|
});
|
|
121
137
|
|
|
122
138
|
it("formats date with milliseconds in America/New_York timezone", () => {
|
|
123
139
|
const date = new Date("2023-05-15T12:00:00.123Z");
|
|
124
|
-
expect(exactDateTime(date, "America/New_York")).toBe(
|
|
140
|
+
expect(exactDateTime(date, "America/New_York", locale)).toBe(
|
|
125
141
|
"2023-05-15 08:00:00.123 EDT",
|
|
126
142
|
);
|
|
127
143
|
});
|
|
@@ -129,42 +145,47 @@ describe("dates", () => {
|
|
|
129
145
|
|
|
130
146
|
describe("timeAgo", () => {
|
|
131
147
|
it("returns empty string for null, undefined, or 0", () => {
|
|
132
|
-
expect(timeAgo(null)).toBe("");
|
|
133
|
-
expect(timeAgo(undefined)).toBe("");
|
|
134
|
-
expect(timeAgo(0)).toBe("");
|
|
148
|
+
expect(timeAgo(null, locale)).toBe("");
|
|
149
|
+
expect(timeAgo(undefined, locale)).toBe("");
|
|
150
|
+
expect(timeAgo(0, locale)).toBe("");
|
|
135
151
|
});
|
|
136
152
|
|
|
137
153
|
it("formats today's date correctly", () => {
|
|
138
154
|
const today = new Date();
|
|
139
|
-
const result = timeAgo(today.toISOString());
|
|
155
|
+
const result = timeAgo(today.toISOString(), locale);
|
|
140
156
|
expect(result).toMatch(/Today at/);
|
|
141
157
|
});
|
|
142
158
|
|
|
143
159
|
it("formats yesterday's date correctly", () => {
|
|
144
160
|
const yesterday = new Date();
|
|
145
161
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
146
|
-
const result = timeAgo(yesterday.toISOString());
|
|
162
|
+
const result = timeAgo(yesterday.toISOString(), locale);
|
|
147
163
|
expect(result).toMatch(/Yesterday at/);
|
|
148
164
|
});
|
|
149
165
|
|
|
150
166
|
it("formats older dates correctly", () => {
|
|
151
167
|
const oldDate = new Date("2020-01-01T12:00:00Z");
|
|
152
|
-
const result = timeAgo(oldDate.toISOString());
|
|
168
|
+
const result = timeAgo(oldDate.toISOString(), locale);
|
|
153
169
|
expect(result).toMatch(/Jan 1, 2020 at/);
|
|
154
170
|
});
|
|
155
171
|
|
|
156
172
|
it("handles errors gracefully", () => {
|
|
157
|
-
expect(timeAgo("invalid-date")).toBe(
|
|
173
|
+
expect(timeAgo("invalid-date", locale)).toBe(
|
|
174
|
+
"Invalid Date at Invalid Date",
|
|
175
|
+
);
|
|
158
176
|
});
|
|
159
177
|
});
|
|
160
178
|
|
|
161
179
|
describe("getShortTimeZone", () => {
|
|
162
180
|
it("returns the short timezone", () => {
|
|
163
|
-
expect(getShortTimeZone("America/New_York")).toBeOneOf([
|
|
181
|
+
expect(getShortTimeZone("America/New_York", locale)).toBeOneOf([
|
|
182
|
+
"EDT",
|
|
183
|
+
"EST",
|
|
184
|
+
]);
|
|
164
185
|
});
|
|
165
186
|
|
|
166
187
|
it("handles errors gracefully", () => {
|
|
167
|
-
expect(getShortTimeZone("MarimoLand")).toBe("MarimoLand");
|
|
188
|
+
expect(getShortTimeZone("MarimoLand", locale)).toBe("MarimoLand");
|
|
168
189
|
});
|
|
169
190
|
});
|
|
170
191
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { EDGE_CASE_FILENAMES } from "../../__tests__/mocks";
|
|
4
5
|
import { Filenames } from "../filenames";
|
|
5
6
|
|
|
6
7
|
describe("Filenames", () => {
|
|
@@ -27,4 +28,21 @@ describe("Filenames", () => {
|
|
|
27
28
|
expect(Filenames.withoutExtension("test.txt")).toEqual("test");
|
|
28
29
|
expect(Filenames.withoutExtension("test.foo.txt")).toEqual("test.foo");
|
|
29
30
|
});
|
|
31
|
+
|
|
32
|
+
it.each(EDGE_CASE_FILENAMES)(
|
|
33
|
+
"should handle edge case filenames: %s",
|
|
34
|
+
(filename) => {
|
|
35
|
+
// Test all filename operations with edge cases
|
|
36
|
+
const withoutExt = Filenames.withoutExtension(filename);
|
|
37
|
+
|
|
38
|
+
expect(Filenames.toMarkdown(filename)).toEqual(`${withoutExt}.md`);
|
|
39
|
+
expect(Filenames.toHTML(filename)).toEqual(`${withoutExt}.html`);
|
|
40
|
+
expect(Filenames.toPNG(filename)).toEqual(`${withoutExt}.png`);
|
|
41
|
+
expect(Filenames.toPY(filename)).toEqual(`${withoutExt}.py`);
|
|
42
|
+
|
|
43
|
+
// Ensure operations preserve unicode and special characters in base name
|
|
44
|
+
expect(withoutExt).not.toEqual("");
|
|
45
|
+
expect(typeof withoutExt).toBe("string");
|
|
46
|
+
},
|
|
47
|
+
);
|
|
30
48
|
});
|
|
@@ -6,24 +6,32 @@ import {
|
|
|
6
6
|
prettyScientificNumber,
|
|
7
7
|
} from "../numbers";
|
|
8
8
|
|
|
9
|
+
const locale = "en-US";
|
|
10
|
+
|
|
9
11
|
describe("prettyNumber", () => {
|
|
10
12
|
it("should format numbers", () => {
|
|
11
|
-
expect(prettyNumber(123_456_789)).toBe("123,456,789");
|
|
12
|
-
expect(prettyNumber(1234.567_89)).toBe("1,234.57");
|
|
13
|
-
expect(prettyNumber(0)).toBe("0");
|
|
13
|
+
expect(prettyNumber(123_456_789, locale)).toBe("123,456,789");
|
|
14
|
+
expect(prettyNumber(1234.567_89, locale)).toBe("1,234.57");
|
|
15
|
+
expect(prettyNumber(0, locale)).toBe("0");
|
|
14
16
|
});
|
|
15
17
|
});
|
|
16
18
|
|
|
19
|
+
const options = { locale };
|
|
20
|
+
|
|
17
21
|
describe("prettyScientificNumber", () => {
|
|
18
22
|
it("should handle special cases", () => {
|
|
19
|
-
expect(prettyScientificNumber(0)).toBe("0");
|
|
20
|
-
expect(prettyScientificNumber(Number.NaN)).toBe("NaN");
|
|
21
|
-
expect(prettyScientificNumber(Number.POSITIVE_INFINITY)).toBe(
|
|
22
|
-
|
|
23
|
+
expect(prettyScientificNumber(0, options)).toBe("0");
|
|
24
|
+
expect(prettyScientificNumber(Number.NaN, options)).toBe("NaN");
|
|
25
|
+
expect(prettyScientificNumber(Number.POSITIVE_INFINITY, options)).toBe(
|
|
26
|
+
"Infinity",
|
|
27
|
+
);
|
|
28
|
+
expect(prettyScientificNumber(Number.NEGATIVE_INFINITY, options)).toBe(
|
|
29
|
+
"-Infinity",
|
|
30
|
+
);
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
it("should format decimals with scientific notation, ignoring integer part rounding", () => {
|
|
26
|
-
const opts = { shouldRound: true };
|
|
34
|
+
const opts = { shouldRound: true, locale };
|
|
27
35
|
expect(prettyScientificNumber(123_456, opts)).toBe("123,456");
|
|
28
36
|
expect(prettyScientificNumber(123_456.7, opts)).toBe("123,456.7");
|
|
29
37
|
expect(prettyScientificNumber(12_345.6789, opts)).toBe("12,345.68");
|
|
@@ -40,10 +48,10 @@ describe("prettyScientificNumber", () => {
|
|
|
40
48
|
});
|
|
41
49
|
|
|
42
50
|
it("should not round numbers when shouldRound is false", () => {
|
|
43
|
-
expect(prettyScientificNumber(123_456)).toBe("123,456");
|
|
44
|
-
expect(prettyScientificNumber(123_456.7)).toBe("123,456.7");
|
|
45
|
-
expect(prettyScientificNumber(12_345.6789)).toBe("12,345.6789");
|
|
46
|
-
expect(prettyScientificNumber(1.234_567_891_011_12)).toBe(
|
|
51
|
+
expect(prettyScientificNumber(123_456, options)).toBe("123,456");
|
|
52
|
+
expect(prettyScientificNumber(123_456.7, options)).toBe("123,456.7");
|
|
53
|
+
expect(prettyScientificNumber(12_345.6789, options)).toBe("12,345.6789");
|
|
54
|
+
expect(prettyScientificNumber(1.234_567_891_011_12, options)).toBe(
|
|
47
55
|
"1.23456789101112",
|
|
48
56
|
);
|
|
49
57
|
});
|
|
@@ -51,26 +59,30 @@ describe("prettyScientificNumber", () => {
|
|
|
51
59
|
|
|
52
60
|
describe("prettyEngineeringNumber", () => {
|
|
53
61
|
it("should handle special cases", () => {
|
|
54
|
-
expect(prettyEngineeringNumber(0)).toBe("0");
|
|
55
|
-
expect(prettyEngineeringNumber(-0)).toBe("0"); // Test with negative zero
|
|
56
|
-
expect(prettyEngineeringNumber(Number.NaN)).toBe("NaN");
|
|
57
|
-
expect(prettyEngineeringNumber(Number.POSITIVE_INFINITY)).toBe(
|
|
58
|
-
|
|
62
|
+
expect(prettyEngineeringNumber(0, locale)).toBe("0");
|
|
63
|
+
expect(prettyEngineeringNumber(-0, locale)).toBe("0"); // Test with negative zero
|
|
64
|
+
expect(prettyEngineeringNumber(Number.NaN, locale)).toBe("NaN");
|
|
65
|
+
expect(prettyEngineeringNumber(Number.POSITIVE_INFINITY, locale)).toBe(
|
|
66
|
+
"Infinity",
|
|
67
|
+
);
|
|
68
|
+
expect(prettyEngineeringNumber(Number.NEGATIVE_INFINITY, locale)).toBe(
|
|
69
|
+
"-Infinity",
|
|
70
|
+
);
|
|
59
71
|
});
|
|
60
72
|
|
|
61
73
|
it("should format decimals with engineering notation, ignoring integer part", () => {
|
|
62
|
-
expect(prettyEngineeringNumber(123_456)).toBe("123k");
|
|
63
|
-
expect(prettyEngineeringNumber(123_456.7)).toBe("123k");
|
|
64
|
-
expect(prettyEngineeringNumber(12_345.6789)).toBe("12.3k");
|
|
65
|
-
expect(prettyEngineeringNumber(1.2345)).toBe("1.23");
|
|
66
|
-
expect(prettyEngineeringNumber(1.000_001_234)).toBe("1");
|
|
67
|
-
expect(prettyEngineeringNumber(0.12)).toBe("120m");
|
|
68
|
-
expect(prettyEngineeringNumber(0.1234)).toBe("123m");
|
|
69
|
-
expect(prettyEngineeringNumber(0.000_123_4)).toBe("123µ");
|
|
70
|
-
expect(prettyEngineeringNumber(-1.2345)).toBe("-1.23"); // Test with negative numbers
|
|
71
|
-
expect(prettyEngineeringNumber(-1.000_001_234)).toBe("-1");
|
|
72
|
-
expect(prettyEngineeringNumber(-0.12)).toBe("-120m");
|
|
73
|
-
expect(prettyEngineeringNumber(-0.1234)).toBe("-123m");
|
|
74
|
-
expect(prettyEngineeringNumber(-0.000_123_4)).toBe("-123µ");
|
|
74
|
+
expect(prettyEngineeringNumber(123_456, locale)).toBe("123k");
|
|
75
|
+
expect(prettyEngineeringNumber(123_456.7, locale)).toBe("123k");
|
|
76
|
+
expect(prettyEngineeringNumber(12_345.6789, locale)).toBe("12.3k");
|
|
77
|
+
expect(prettyEngineeringNumber(1.2345, locale)).toBe("1.23");
|
|
78
|
+
expect(prettyEngineeringNumber(1.000_001_234, locale)).toBe("1");
|
|
79
|
+
expect(prettyEngineeringNumber(0.12, locale)).toBe("120m");
|
|
80
|
+
expect(prettyEngineeringNumber(0.1234, locale)).toBe("123m");
|
|
81
|
+
expect(prettyEngineeringNumber(0.000_123_4, locale)).toBe("123µ");
|
|
82
|
+
expect(prettyEngineeringNumber(-1.2345, locale)).toBe("-1.23"); // Test with negative numbers
|
|
83
|
+
expect(prettyEngineeringNumber(-1.000_001_234, locale)).toBe("-1");
|
|
84
|
+
expect(prettyEngineeringNumber(-0.12, locale)).toBe("-120m");
|
|
85
|
+
expect(prettyEngineeringNumber(-0.1234, locale)).toBe("-123m");
|
|
86
|
+
expect(prettyEngineeringNumber(-0.000_123_4, locale)).toBe("-123µ");
|
|
75
87
|
});
|
|
76
88
|
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/* Copyright 2024 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { memoizeLastValue, once } from "../once";
|
|
4
|
+
|
|
5
|
+
describe("once", () => {
|
|
6
|
+
it("should call function only once", () => {
|
|
7
|
+
const fn = vi.fn(() => "result");
|
|
8
|
+
const onceFn = once(fn);
|
|
9
|
+
|
|
10
|
+
const result1 = onceFn();
|
|
11
|
+
const result2 = onceFn();
|
|
12
|
+
|
|
13
|
+
expect(result1).toBe("result");
|
|
14
|
+
expect(result2).toBe("result");
|
|
15
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should return the same result on subsequent calls", () => {
|
|
19
|
+
let counter = 0;
|
|
20
|
+
const fn = () => ++counter;
|
|
21
|
+
const onceFn = once(fn);
|
|
22
|
+
|
|
23
|
+
expect(onceFn()).toBe(1);
|
|
24
|
+
expect(onceFn()).toBe(1);
|
|
25
|
+
expect(onceFn()).toBe(1);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("memoizeLastValue", () => {
|
|
30
|
+
it("should memoize result for same arguments", () => {
|
|
31
|
+
const fn = vi.fn((a: number, b: string) => `${a}-${b}`);
|
|
32
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
33
|
+
|
|
34
|
+
const result1 = memoizedFn(1, "test");
|
|
35
|
+
const result2 = memoizedFn(1, "test");
|
|
36
|
+
|
|
37
|
+
expect(result1).toBe("1-test");
|
|
38
|
+
expect(result2).toBe("1-test");
|
|
39
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should recompute for different arguments", () => {
|
|
43
|
+
const fn = vi.fn((a: number) => a * 2);
|
|
44
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
45
|
+
|
|
46
|
+
const result1 = memoizedFn(5);
|
|
47
|
+
const result2 = memoizedFn(10);
|
|
48
|
+
const result3 = memoizedFn(5); // Should recompute since args changed from previous call
|
|
49
|
+
|
|
50
|
+
expect(result1).toBe(10);
|
|
51
|
+
expect(result2).toBe(20);
|
|
52
|
+
expect(result3).toBe(10);
|
|
53
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("should handle no arguments", () => {
|
|
57
|
+
const fn = vi.fn(() => "no-args");
|
|
58
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
59
|
+
|
|
60
|
+
const result1 = memoizedFn();
|
|
61
|
+
const result2 = memoizedFn();
|
|
62
|
+
|
|
63
|
+
expect(result1).toBe("no-args");
|
|
64
|
+
expect(result2).toBe("no-args");
|
|
65
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should handle single argument", () => {
|
|
69
|
+
const fn = vi.fn((x: number) => x + 1);
|
|
70
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
71
|
+
|
|
72
|
+
expect(memoizedFn(5)).toBe(6);
|
|
73
|
+
expect(memoizedFn(5)).toBe(6);
|
|
74
|
+
expect(memoizedFn(3)).toBe(4);
|
|
75
|
+
|
|
76
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should handle multiple arguments", () => {
|
|
80
|
+
const fn = vi.fn((a: number, b: string, c: boolean) => ({ a, b, c }));
|
|
81
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
82
|
+
|
|
83
|
+
const result1 = memoizedFn(1, "hello", true);
|
|
84
|
+
const result2 = memoizedFn(1, "hello", true);
|
|
85
|
+
const result3 = memoizedFn(1, "hello", false);
|
|
86
|
+
|
|
87
|
+
expect(result1).toEqual({ a: 1, b: "hello", c: true });
|
|
88
|
+
expect(result2).toEqual({ a: 1, b: "hello", c: true });
|
|
89
|
+
expect(result3).toEqual({ a: 1, b: "hello", c: false });
|
|
90
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should use shallow comparison for objects", () => {
|
|
94
|
+
const fn = vi.fn((obj: { x: number }) => obj.x * 2);
|
|
95
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
96
|
+
|
|
97
|
+
const obj1 = { x: 5 };
|
|
98
|
+
const obj2 = { x: 5 }; // Different reference but same content
|
|
99
|
+
|
|
100
|
+
const result1 = memoizedFn(obj1);
|
|
101
|
+
const result2 = memoizedFn(obj1); // Same reference
|
|
102
|
+
const result3 = memoizedFn(obj2); // Different reference
|
|
103
|
+
|
|
104
|
+
expect(result1).toBe(10);
|
|
105
|
+
expect(result2).toBe(10);
|
|
106
|
+
expect(result3).toBe(10);
|
|
107
|
+
expect(fn).toHaveBeenCalledTimes(2); // obj1 (twice with same ref) and obj2 (different ref)
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should handle arrays", () => {
|
|
111
|
+
const fn = vi.fn((arr: number[]) => arr.reduce((sum, x) => sum + x, 0));
|
|
112
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
113
|
+
|
|
114
|
+
const result1 = memoizedFn([1, 2, 3]);
|
|
115
|
+
const result2 = memoizedFn([1, 2, 3]); // Different array reference but same content
|
|
116
|
+
const result3 = memoizedFn([1, 2, 4]); // Different content
|
|
117
|
+
|
|
118
|
+
expect(result1).toBe(6);
|
|
119
|
+
expect(result2).toBe(6);
|
|
120
|
+
expect(result3).toBe(7);
|
|
121
|
+
expect(fn).toHaveBeenCalledTimes(3); // Each call has different array reference
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should handle mixed argument types", () => {
|
|
125
|
+
const fn = vi.fn(
|
|
126
|
+
(num: number, str: string, arr: number[], obj: { key: string }) =>
|
|
127
|
+
`${num}-${str}-${arr.length}-${obj.key}`,
|
|
128
|
+
);
|
|
129
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
130
|
+
|
|
131
|
+
const arr = [1, 2, 3];
|
|
132
|
+
const obj = { key: "test" };
|
|
133
|
+
|
|
134
|
+
const result1 = memoizedFn(42, "hello", arr, obj);
|
|
135
|
+
const result2 = memoizedFn(42, "hello", arr, obj); // Same references
|
|
136
|
+
const result3 = memoizedFn(42, "hello", [1, 2, 3], { key: "test" }); // Different references
|
|
137
|
+
|
|
138
|
+
expect(result1).toBe("42-hello-3-test");
|
|
139
|
+
expect(result2).toBe("42-hello-3-test");
|
|
140
|
+
expect(result3).toBe("42-hello-3-test");
|
|
141
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should handle undefined and null arguments", () => {
|
|
145
|
+
const fn = vi.fn((a?: string, b?: null) => `${a}-${b}`);
|
|
146
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
147
|
+
|
|
148
|
+
const result1 = memoizedFn(undefined, null);
|
|
149
|
+
const result2 = memoizedFn(undefined, null);
|
|
150
|
+
const result3 = memoizedFn("test", null);
|
|
151
|
+
|
|
152
|
+
expect(result1).toBe("undefined-null");
|
|
153
|
+
expect(result2).toBe("undefined-null");
|
|
154
|
+
expect(result3).toBe("test-null");
|
|
155
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should preserve function context", () => {
|
|
159
|
+
const context = {
|
|
160
|
+
value: 10,
|
|
161
|
+
fn: vi.fn(function (this: { value: number }, multiplier: number) {
|
|
162
|
+
return this.value * multiplier;
|
|
163
|
+
}),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const memoizedFn = memoizeLastValue(context.fn);
|
|
167
|
+
|
|
168
|
+
const result1 = memoizedFn.call(context, 2);
|
|
169
|
+
const result2 = memoizedFn.call(context, 2);
|
|
170
|
+
|
|
171
|
+
expect(result1).toBe(20);
|
|
172
|
+
expect(result2).toBe(20);
|
|
173
|
+
expect(context.fn).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should handle functions that throw errors", () => {
|
|
177
|
+
const error = new Error("test error");
|
|
178
|
+
const fn = vi.fn(() => {
|
|
179
|
+
throw error;
|
|
180
|
+
});
|
|
181
|
+
const memoizedFn = memoizeLastValue(fn);
|
|
182
|
+
|
|
183
|
+
expect(() => memoizedFn()).toThrow("test error");
|
|
184
|
+
expect(() => memoizedFn()).toThrow("test error"); // Should throw cached error
|
|
185
|
+
expect(fn).toHaveBeenCalledTimes(1); // Error should be memoized too
|
|
186
|
+
});
|
|
187
|
+
});
|