@marimo-team/frontend 0.23.16-dev2 → 0.23.16-dev21
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/assets/{CellStatus-BalYv0pK.js → CellStatus-DwV4gbV3.js} +1 -1
- package/dist/assets/{ConnectedDataExplorerComponent-DI7mV-Gw.js → ConnectedDataExplorerComponent-CMaZapRd.js} +1 -1
- package/dist/assets/{ErrorBoundary-C7_5jjWi.js → ErrorBoundary-DzTD8LVq.js} +1 -1
- package/dist/assets/{ImperativeModal-BIavOiUv.js → ImperativeModal-fY-ZfT0b.js} +1 -1
- package/dist/assets/JsonOutput-BoDVlAbV.js +53 -0
- package/dist/assets/{LazyAnyLanguageCodeMirror-sVm_hz8A.js → LazyAnyLanguageCodeMirror-B2JvSXP-.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DO2k2fA5.js → MarimoErrorOutput-C2HrF0bD.js} +5 -5
- package/dist/assets/{RSPContexts-CgC81f0I.js → RSPContexts-WiCgGm__.js} +1 -1
- package/dist/assets/RenderHTML-DoCFSir5.js +1 -0
- package/dist/assets/{RunButton-BKRupqr7.js → RunButton-COtmJuup.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CEZtaZCK.js → add-cell-with-ai-96GHR2R7.js} +16 -16
- package/dist/assets/add-connection-dialog-wLF3on0N.js +170 -0
- package/dist/assets/{agent-panel-By4A-Cqr.js → agent-panel-xLPKpIwr.js} +5 -5
- package/dist/assets/ai-model-dropdown-CR8HQGYf.js +5 -0
- package/dist/assets/{alert-dialog-BXegNC0I.js → alert-dialog-Bb8Lz8ko.js} +1 -1
- package/dist/assets/{any-language-editor-DCpZRrpx.js → any-language-editor--tRHs8C_.js} +1 -1
- package/dist/assets/{app-config-button-NrM9ywXA.js → app-config-button-Ch-fmU-u.js} +1 -1
- package/dist/assets/{button-eIihcbUw.js → button-DDGiD1NW.js} +1 -1
- package/dist/assets/{cache-panel-wllSTlbM.js → cache-panel-DcQUeUpg.js} +1 -1
- package/dist/assets/{capabilities-B09j1Eor.js → capabilities-DoEg0XwL.js} +1 -1
- package/dist/assets/{cell-editor-DdofA9oQ.js → cell-editor-CxP3SFwW.js} +10 -10
- package/dist/assets/cell-link-D1nZ5myN.js +1 -0
- package/dist/assets/cells-D9tCNe6o.js +231 -0
- package/dist/assets/{chat-display-BG08ltQ3.js → chat-display-PstK5lnL.js} +1 -1
- package/dist/assets/{chat-panel-P2CzCtXJ.js → chat-panel-XuGXjT-8.js} +3 -3
- package/dist/assets/{chat-ui-fAJZAoTD.js → chat-ui-Cme86OAK.js} +1 -1
- package/dist/assets/{column-preview-BgZLUEOj.js → column-preview-BYiduGzU.js} +1 -1
- package/dist/assets/{command-CAJLPEBq.js → command-Dqxie4gq.js} +1 -1
- package/dist/assets/{command-palette-DrI24FoL.js → command-palette-C7NlQPUx.js} +1 -1
- package/dist/assets/{common-CD6Mj8n6.js → common-XBd1qRDk.js} +1 -1
- package/dist/assets/components-D-aPO6md.js +1 -0
- package/dist/assets/config-B-Q7J_6d.js +1 -0
- package/dist/assets/{config-COUOIPEq.js → config-BJVx3eBW.js} +1 -1
- package/dist/assets/{copy-BtTOqPTS.js → copy-Bxb1_Gtw.js} +1 -1
- package/dist/assets/{copy-icon-C2pG0ZNK.js → copy-icon-neDQUo4Q.js} +1 -1
- package/dist/assets/{createReducer-C7kZ8zEe.js → createReducer-DGx5Af-C.js} +1 -1
- package/dist/assets/{datasource-ySAiC9gr.js → datasource-Pl4KKOUa.js} +2 -2
- package/dist/assets/{dates-D-kIKDYA.js → dates-C2LWzsCZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-BF23EwcD.js → dependency-graph-panel-98TlJ5y7.js} +4 -4
- package/dist/assets/{dialog-D1DEsRZ2.js → dialog-BCVepWLJ.js} +1 -1
- package/dist/assets/{dist-ByVeMWkr.js → dist-BLN8PNJV.js} +1 -1
- package/dist/assets/dist-CiVShZKM.js +2 -0
- package/dist/assets/documentation-panel-DSOMQtKU.js +1 -0
- package/dist/assets/{download-BomYt3nl.js → download-CnYLah74.js} +3 -3
- package/dist/assets/{dropdown-menu-BFZLnWeO.js → dropdown-menu-dHmZR8bI.js} +1 -1
- package/dist/assets/{edit-page-C37w54dQ.js → edit-page-CchyCkOl.js} +6 -6
- package/dist/assets/{error-banner-B6eIlxzg.js → error-banner-BSxEOIn2.js} +1 -1
- package/dist/assets/{error-entries-Birw-8VX.js → error-entries-CmfGHGq1.js} +3 -3
- package/dist/assets/{error-panel-DdsUL6b7.js → error-panel-BKb_UksO.js} +1 -1
- package/dist/assets/{es-1D83cgiu.js → es-gYgfF8y5.js} +1 -1
- package/dist/assets/{esm-BL7jQmzn.js → esm-B30N1GS0.js} +1 -1
- package/dist/assets/{field-BeZbVzXA.js → field-BsxD35VE.js} +1 -1
- package/dist/assets/file-explorer-panel-8_ps5wdU.js +26 -0
- package/dist/assets/{file-icons-Cb54mIL8.js → file-icons-Czm7CWoc.js} +1 -1
- package/dist/assets/{file-name-input-BWFcK1ey.js → file-name-input-CjWG0FWT.js} +1 -1
- package/dist/assets/floating-outline-BWk7h_Uf.js +1 -0
- package/dist/assets/focus-DTmSq3hN.js +1 -0
- package/dist/assets/{form-CgWNb44_.js → form-B5UJtZJr.js} +1 -1
- package/dist/assets/{formats-ClEFGPy-.js → formats-DzmLpc9A.js} +1 -1
- package/dist/assets/{formatting-B_41gSF5.js → formatting-CDf6w1Kt.js} +1 -1
- package/dist/assets/{fullscreen-dUjPYOmm.js → fullscreen-CTDqNQr6.js} +1 -1
- package/dist/assets/{gallery-page-CWQQAYpa.js → gallery-page-CJ3ms1Hn.js} +1 -1
- package/dist/assets/{glide-data-editor-lMARFWHS.js → glide-data-editor-j2igs9Y_.js} +1 -1
- package/dist/assets/home-page-cBU6sDBU.js +4 -0
- package/dist/assets/{html-to-image-CwmX28T9.js → html-to-image-1vflBYah.js} +2 -2
- package/dist/assets/index-B8eK-daS.js +38 -0
- package/dist/assets/index-xs-zSSrX.css +2 -0
- package/dist/assets/{input-CqrUjynb.js → input-JxFwu-3G.js} +1 -1
- package/dist/assets/kiosk-mode-BumOFEtY.js +1 -0
- package/dist/assets/{label-BElwWzf0.js → label-CqjrpwRC.js} +1 -1
- package/dist/assets/{layout-BOwE7yok.js → layout-DkpUSblm.js} +5 -5
- package/dist/assets/{links-CToBjIIj.js → links-CZr5bjvo.js} +1 -1
- package/dist/assets/{logs-panel-iFV4ZJ9b.js → logs-panel-BojuPimp.js} +1 -1
- package/dist/assets/{markdown-renderer-QvXG0CRk.js → markdown-renderer-C9vdWclk.js} +2 -2
- package/dist/assets/{menu-items-DbSLBfXR.js → menu-items-CCi6aOPm.js} +1 -1
- package/dist/assets/{mermaid-Bh0flEIb.js → mermaid-CYN0wOyG.js} +1 -1
- package/dist/assets/name-cell-input-BSFsnuTp.js +1 -0
- package/dist/assets/node-sql-parser-Si4oZ2P-.js +68 -0
- package/dist/assets/{numbers-CeeaFKqu.js → numbers-rmawRqQP.js} +1 -1
- package/dist/assets/outline-panel-B1f5a2yI.js +1 -0
- package/dist/assets/{packages-panel-BzQF60rG.js → packages-panel-BKuXx1gi.js} +1 -1
- package/dist/assets/{pair-with-agent-modal-CKOYwOYw.js → pair-with-agent-modal-nwd26aKZ.js} +1 -1
- package/dist/assets/panels-Cvyb3kny.js +4 -0
- package/dist/assets/{popover-DrHQ3KEt.js → popover-Bcz1Zikb.js} +1 -1
- package/dist/assets/process-output-Csd6PmBH.js +1 -0
- package/dist/assets/{radio-group-CLBjGkt5.js → radio-group-CdR34W0K.js} +1 -1
- package/dist/assets/readonly-code-display-XBAHhCYR.js +1 -0
- package/dist/assets/{readonly-python-code-CW_G5e4E.js → readonly-python-code-BqcMp63O.js} +1 -1
- package/dist/assets/{renderShortcut-BZLr8D7H.js → renderShortcut-Bl8_FVn8.js} +1 -1
- package/dist/assets/{reveal-component-DqMRxmUr.js → reveal-component-BvsuzhVi.js} +57 -57
- package/dist/assets/{run-page-s6Mf8p78.js → run-page-B9jHpo8w.js} +1 -1
- package/dist/assets/{runs-D51Xwrsk.js → runs-DBTXqJHS.js} +1 -1
- package/dist/assets/scratchpad-panel-D3slJkhR.js +1 -0
- package/dist/assets/{scroll-to-bottom-button-Cgr10kaJ.js → scroll-to-bottom-button-C8yfT4wq.js} +1 -1
- package/dist/assets/{secrets-panel-Cc5KDBk6.js → secrets-panel-6J6wcuVR.js} +1 -1
- package/dist/assets/{select-B5TFNjmU.js → select-BP75_5jJ.js} +1 -1
- package/dist/assets/{session-CEFnN9Cq.js → session-BmukRjSi.js} +1 -1
- package/dist/assets/{session-panel-CmHetqAb.js → session-panel-DO-4U6Ry.js} +1 -1
- package/dist/assets/{share-CYpcX4N4.js → share-852X_cqy.js} +1 -1
- package/dist/assets/{skeleton-B_4CZVEm.js → skeleton-C3fwwBky.js} +1 -1
- package/dist/assets/{snippets-panel-CXpGflMO.js → snippets-panel-ACMhA8oL.js} +1 -1
- package/dist/assets/{spec-CzqcmYQk.js → spec-C7opZZRp.js} +1 -1
- package/dist/assets/{state-BvdK8eiw.js → state-D10U2MwA.js} +1 -1
- package/dist/assets/{state-BXbiRXZk.js → state-D2ccy8VQ.js} +3 -3
- package/dist/assets/{state-WBgc7MiA.js → state-DDag0B_B.js} +1 -1
- package/dist/assets/{state-DBzEQtWz.js → state-N0U-K9F6.js} +1 -1
- package/dist/assets/{strings-Djnbwmfl.js → strings--BknHX9C.js} +1 -1
- package/dist/assets/{swiper-component-DInvCxhI.js → swiper-component-Che9MNs4.js} +1 -1
- package/dist/assets/{switch-CvweaCXq.js → switch-Drt3XC71.js} +1 -1
- package/dist/assets/{terminal-D3IQBBYo.js → terminal-217O8hlZ.js} +1 -1
- package/dist/assets/{textarea-CpFfEUet.js → textarea-BQE_pb7P.js} +1 -1
- package/dist/assets/{tooltip-CFX4fYHU.js → tooltip-4bZlU7jl.js} +1 -1
- package/dist/assets/{tooltip-HLb6RxVp.js → tooltip-B7U4xkIN.js} +1 -1
- package/dist/assets/{tracing-CKn3gWHw.js → tracing-CuJfR83E.js} +1 -1
- package/dist/assets/{tracing-panel-BU-o_LCd.js → tracing-panel-CmMG0R-H.js} +2 -2
- package/dist/assets/{tree-actions-CsC2XmGo.js → tree-actions-DJTGQWz0.js} +1 -1
- package/dist/assets/{useBoolean-D89u30n-.js → useBoolean-CFj86iac.js} +1 -1
- package/dist/assets/useCellActionButton-BhCYWOKS.js +1 -0
- package/dist/assets/{useDeleteCell-EPH4kcG7.js → useDeleteCell-DsWH8wmn.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-CZnK7N2L.js → useDependencyPanelTab-CAI77Oxj.js} +1 -1
- package/dist/assets/useEventListener-C0195JsF.js +1 -0
- package/dist/assets/{useHotkey-DIuRH8Wu.js → useHotkey-DLZvrRZT.js} +1 -1
- package/dist/assets/{useIframeCapabilities-Ccb4fvKc.js → useIframeCapabilities-j6fR_dBA.js} +1 -1
- package/dist/assets/{useInstallPackage-BRbUAUFn.js → useInstallPackage-WiYBwhUO.js} +1 -1
- package/dist/assets/{useInterval-CC0vKpWm.js → useInterval-CWBTUy5W.js} +1 -1
- package/dist/assets/{useNotebookActions-DJKDd0gM.js → useNotebookActions-9T9wvd8s.js} +4 -4
- package/dist/assets/useRunCells-BpMmDGqa.js +1 -0
- package/dist/assets/{useSplitCell-9Qz93yPZ.js → useSplitCell-CapBPXO1.js} +1 -1
- package/dist/assets/{useTheme-KXqAyDPC.js → useTheme-Bxv9B31g.js} +1 -1
- package/dist/assets/{utils-Z80SV4_d.js → utils-BWBBpCkT.js} +1 -1
- package/dist/assets/{vega-component-Cie3ag_T.js → vega-component-Db8KjZJG.js} +1 -1
- package/dist/assets/{write-secret-modal-DEEn7sZV.js → write-secret-modal-3-v-dHVj.js} +1 -1
- package/dist/index.html +68 -68
- package/package.json +2 -2
- package/src/components/data-table/column-formatting/feature.ts +2 -1
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
- package/src/components/editor/connections/storage/as-code.ts +40 -21
- package/src/components/editor/connections/storage/schemas.ts +83 -42
- package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
- package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
- package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
- package/src/components/editor/file-tree/file-render-mode.ts +32 -0
- package/src/components/editor/file-tree/file-viewer.tsx +76 -50
- package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +3 -2
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/ui/input.tsx +1 -1
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/i18n/__tests__/locale-provider.test.tsx +54 -118
- package/src/core/i18n/__tests__/locale.test.ts +113 -0
- package/src/core/i18n/locale-provider.tsx +1 -19
- package/src/core/i18n/locale.ts +71 -0
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bridge.ts +15 -0
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-network.test.ts +13 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-network.ts +6 -3
- package/src/core/network/types.ts +3 -1
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/dist/assets/JsonOutput-DnGSkLeP.js +0 -53
- package/dist/assets/RenderHTML-BCvU9dYz.js +0 -1
- package/dist/assets/add-connection-dialog-CEp3SJR2.js +0 -170
- package/dist/assets/ai-model-dropdown-DXgLphFa.js +0 -5
- package/dist/assets/cell-link-DqBkyAFe.js +0 -1
- package/dist/assets/cells-Cf7bHHq3.js +0 -229
- package/dist/assets/components-BH6gDVv6.js +0 -1
- package/dist/assets/config-BbOTTCYY.js +0 -1
- package/dist/assets/dist-BCMBu0rO.js +0 -2
- package/dist/assets/documentation-panel-e9Fi9R7e.js +0 -1
- package/dist/assets/file-explorer-panel-DbdoJbbF.js +0 -26
- package/dist/assets/floating-outline-wz-WSRgP.js +0 -1
- package/dist/assets/focus-DiEyfiAG.js +0 -1
- package/dist/assets/home-page-DJ4jJsZj.js +0 -4
- package/dist/assets/index-A71LAt6h.css +0 -2
- package/dist/assets/index-Cl0lNhQU.js +0 -38
- package/dist/assets/kiosk-mode-ABm3oYgq.js +0 -1
- package/dist/assets/name-cell-input-C6cukQ9k.js +0 -1
- package/dist/assets/node-sql-parser-BWgUjcfV.js +0 -68
- package/dist/assets/outline-panel-BPlg9_CE.js +0 -1
- package/dist/assets/panels-CW-PUlv2.js +0 -4
- package/dist/assets/process-output-BEhIGfkO.js +0 -1
- package/dist/assets/readonly-code-display-D63oRtBP.js +0 -1
- package/dist/assets/scratchpad-panel-CXsxdMB-.js +0 -1
- package/dist/assets/useCellActionButton-CRfIEVB5.js +0 -1
- package/dist/assets/useEventListener-B_w3A283.js +0 -1
- package/dist/assets/useRunCells-CSSF_mJ0.js +0 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Locale used when neither the configured locale nor the browser locale can be
|
|
5
|
+
* resolved to a valid BCP 47 tag.
|
|
6
|
+
*/
|
|
7
|
+
export const FALLBACK_LOCALE = "en-US";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Whether a tag is accepted by `Intl` (and therefore by react-aria's
|
|
11
|
+
* `I18nProvider`, which constructs `Intl.Locale` internally).
|
|
12
|
+
*/
|
|
13
|
+
export function isValidLocale(locale: string): boolean {
|
|
14
|
+
if (!locale) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
new Intl.Locale(locale);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Coerce a browser or config locale tag into a valid
|
|
27
|
+
* [BCP 47](https://www.rfc-editor.org/info/rfc5646) tag, or `null` if nothing
|
|
28
|
+
* usable can be recovered.
|
|
29
|
+
*
|
|
30
|
+
* Some environments report POSIX-style tags that `Intl` rejects with
|
|
31
|
+
* `RangeError: Incorrect locale information provided`, crashing anything that
|
|
32
|
+
* constructs an `Intl.*` formatter (see issue #9938). Known offenders:
|
|
33
|
+
* - POSIX modifiers, e.g. Chromium under Playwright reporting `en-US@posix`
|
|
34
|
+
* - charset suffixes, e.g. `en_US.UTF-8`
|
|
35
|
+
* - underscore separators, e.g. `en_US`
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeLocale(tag: string | null | undefined): string | null {
|
|
38
|
+
if (!tag) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const candidate = tag.split("@")[0].split(".")[0].trim().replaceAll("_", "-");
|
|
43
|
+
|
|
44
|
+
if (isValidLocale(candidate)) {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fall back to the language subtag alone (e.g. `en` from a mangled `en-XX`).
|
|
49
|
+
const language = candidate.split("-")[0];
|
|
50
|
+
if (isValidLocale(language)) {
|
|
51
|
+
return language;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Resolve the browser's locale to a safe BCP 47 tag.
|
|
58
|
+
* See https://github.com/marimo-team/marimo/issues/9938 */
|
|
59
|
+
export function browserLocale(): string {
|
|
60
|
+
const language =
|
|
61
|
+
typeof navigator === "undefined" ? undefined : navigator.language;
|
|
62
|
+
return normalizeLocale(language) ?? FALLBACK_LOCALE;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve a configured locale to a safe BCP 47 tag, preferring the config and
|
|
67
|
+
* falling back to the browser locale, then to {@link FALLBACK_LOCALE}.
|
|
68
|
+
*/
|
|
69
|
+
export function safeLocale(locale: string | null | undefined): string {
|
|
70
|
+
return normalizeLocale(locale) ?? browserLocale();
|
|
71
|
+
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@/__tests__/branded";
|
|
11
11
|
import { notebookAtom } from "@/core/cells/cells";
|
|
12
12
|
import { islandsPendingInitialRunsAtom } from "@/core/islands/state";
|
|
13
|
+
import type { IslandsKernelMessage } from "../worker/worker";
|
|
13
14
|
|
|
14
15
|
type Base64String = components["schemas"]["Base64String"];
|
|
15
16
|
interface TestIslandApp {
|
|
@@ -173,6 +174,19 @@ describe("IslandsPyodideBridge", () => {
|
|
|
173
174
|
});
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
function sendKernelMessage(sessionGeneration: number) {
|
|
178
|
+
const listener = mockMessageListeners.get("kernelMessage");
|
|
179
|
+
if (!listener) {
|
|
180
|
+
throw new Error("Missing kernel message listener");
|
|
181
|
+
}
|
|
182
|
+
const message = {
|
|
183
|
+
message: "" as IslandsKernelMessage["message"],
|
|
184
|
+
sessionGeneration,
|
|
185
|
+
};
|
|
186
|
+
listener(message as never);
|
|
187
|
+
return message;
|
|
188
|
+
}
|
|
189
|
+
|
|
176
190
|
async function initializeSingleApp() {
|
|
177
191
|
mockSingleApp();
|
|
178
192
|
await bridge.initializeApps();
|
|
@@ -281,6 +295,99 @@ describe("IslandsPyodideBridge", () => {
|
|
|
281
295
|
});
|
|
282
296
|
|
|
283
297
|
describe("app lifecycle", () => {
|
|
298
|
+
it("ignores messages after a session yields ownership", async () => {
|
|
299
|
+
const consumeMessage = vi.fn();
|
|
300
|
+
bridge.consumeMessages(consumeMessage);
|
|
301
|
+
mockSingleApp();
|
|
302
|
+
await bridge.initializeApps();
|
|
303
|
+
|
|
304
|
+
const firstMessage = sendKernelMessage(1);
|
|
305
|
+
let finishReplacement!: () => void;
|
|
306
|
+
mockReplaceSessionRequest.mockReturnValueOnce(
|
|
307
|
+
new Promise<void>((resolve) => {
|
|
308
|
+
finishReplacement = resolve;
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
setSingleApp("generated app 2");
|
|
312
|
+
const replacement = bridge.initializeApps();
|
|
313
|
+
await vi.waitFor(() =>
|
|
314
|
+
expect(mockReplaceSessionRequest).toHaveBeenCalledOnce(),
|
|
315
|
+
);
|
|
316
|
+
sendKernelMessage(1);
|
|
317
|
+
const currentMessage = sendKernelMessage(2);
|
|
318
|
+
|
|
319
|
+
expect(consumeMessage.mock.calls).toEqual([
|
|
320
|
+
[firstMessage],
|
|
321
|
+
[currentMessage],
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
finishReplacement();
|
|
325
|
+
await replacement;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("restores message ownership when replacement fails", async () => {
|
|
329
|
+
const consumeMessage = vi.fn();
|
|
330
|
+
bridge.consumeMessages(consumeMessage);
|
|
331
|
+
mockSingleApp();
|
|
332
|
+
await bridge.initializeApps();
|
|
333
|
+
mockReplaceSessionRequest.mockRejectedValueOnce(
|
|
334
|
+
new Error("replacement failed"),
|
|
335
|
+
);
|
|
336
|
+
setSingleApp("generated app 2");
|
|
337
|
+
|
|
338
|
+
await expect(bridge.initializeApps()).rejects.toThrow(
|
|
339
|
+
"replacement failed",
|
|
340
|
+
);
|
|
341
|
+
const previousMessage = sendKernelMessage(1);
|
|
342
|
+
sendKernelMessage(2);
|
|
343
|
+
|
|
344
|
+
expect(consumeMessage.mock.calls).toEqual([[previousMessage]]);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("ignores messages while their session is stopping", async () => {
|
|
348
|
+
const consumeMessage = vi.fn();
|
|
349
|
+
bridge.consumeMessages(consumeMessage);
|
|
350
|
+
mockSingleApp();
|
|
351
|
+
await bridge.initializeApps();
|
|
352
|
+
let finishStop!: () => void;
|
|
353
|
+
mockStopSessionRequest.mockReturnValueOnce(
|
|
354
|
+
new Promise<void>((resolve) => {
|
|
355
|
+
finishStop = resolve;
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const stop = bridge.stopSession();
|
|
360
|
+
await vi.waitFor(() =>
|
|
361
|
+
expect(mockStopSessionRequest).toHaveBeenCalledOnce(),
|
|
362
|
+
);
|
|
363
|
+
sendKernelMessage(1);
|
|
364
|
+
|
|
365
|
+
expect(consumeMessage).not.toHaveBeenCalled();
|
|
366
|
+
|
|
367
|
+
finishStop();
|
|
368
|
+
await stop;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("forwards messages from every app in a multi-app document", async () => {
|
|
372
|
+
const consumeMessage = vi.fn();
|
|
373
|
+
bridge.consumeMessages(consumeMessage);
|
|
374
|
+
mockParseMarimoIslandApps.mockReturnValue([app(), app("app-2")]);
|
|
375
|
+
mockCreateMarimoFile
|
|
376
|
+
.mockReturnValueOnce("generated app 1")
|
|
377
|
+
.mockReturnValueOnce("generated app 2");
|
|
378
|
+
signalWorkerReady();
|
|
379
|
+
|
|
380
|
+
await bridge.initializeApps();
|
|
381
|
+
const firstMessage = sendKernelMessage(1);
|
|
382
|
+
const secondMessage = sendKernelMessage(2);
|
|
383
|
+
sendKernelMessage(3);
|
|
384
|
+
|
|
385
|
+
expect(consumeMessage.mock.calls).toEqual([
|
|
386
|
+
[firstMessage],
|
|
387
|
+
[secondMessage],
|
|
388
|
+
]);
|
|
389
|
+
});
|
|
390
|
+
|
|
284
391
|
it("holds controls until the first app session is ready", async () => {
|
|
285
392
|
setSingleApp();
|
|
286
393
|
|
|
@@ -66,6 +66,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
66
66
|
private session: AppSession | undefined;
|
|
67
67
|
private sessionReady = new Deferred<AppSession>();
|
|
68
68
|
private nextSessionGeneration = 0;
|
|
69
|
+
private readonly activeSessionGenerations = new Set<number>();
|
|
69
70
|
private appTransition = Promise.resolve();
|
|
70
71
|
private workerReady = new Deferred<void>();
|
|
71
72
|
|
|
@@ -110,6 +111,11 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
110
111
|
);
|
|
111
112
|
|
|
112
113
|
this.rpc.addMessageListener("kernelMessage", (message) => {
|
|
114
|
+
// A stopped session can still have messages in transit after the next
|
|
115
|
+
// app takes ownership of the shared frontend state.
|
|
116
|
+
if (!this.activeSessionGenerations.has(message.sessionGeneration)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
113
119
|
this.messageConsumer?.(message);
|
|
114
120
|
});
|
|
115
121
|
}
|
|
@@ -149,6 +155,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
149
155
|
return;
|
|
150
156
|
}
|
|
151
157
|
if (apps.length > 0) {
|
|
158
|
+
this.activeSessionGenerations.clear();
|
|
152
159
|
this.store.set(
|
|
153
160
|
islandsPendingInitialRunsAtom,
|
|
154
161
|
new Set(apps.map((_, index) => this.nextSessionGeneration + index + 1)),
|
|
@@ -165,6 +172,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
165
172
|
};
|
|
166
173
|
const previousSession = this.session;
|
|
167
174
|
const replacesSession = managesSingleApp && previousSession !== undefined;
|
|
175
|
+
this.activeSessionGenerations.add(request.sessionGeneration);
|
|
168
176
|
if (replacesSession) {
|
|
169
177
|
this.store.set(notebookAtom, initialNotebookState());
|
|
170
178
|
}
|
|
@@ -186,8 +194,14 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
186
194
|
this.sessionReady.resolve(this.session);
|
|
187
195
|
}
|
|
188
196
|
} catch (error) {
|
|
197
|
+
this.activeSessionGenerations.delete(request.sessionGeneration);
|
|
189
198
|
if (this.session?.sessionGeneration === request.sessionGeneration) {
|
|
190
199
|
this.session = previousSession;
|
|
200
|
+
if (previousSession) {
|
|
201
|
+
this.activeSessionGenerations.add(
|
|
202
|
+
previousSession.sessionGeneration,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
191
205
|
}
|
|
192
206
|
Logger.error(`Failed to start session for app ${app.id}:`, error);
|
|
193
207
|
if (managesSingleApp) {
|
|
@@ -203,6 +217,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
203
217
|
if (session?.code === undefined || (appId && session.appId !== appId)) {
|
|
204
218
|
return;
|
|
205
219
|
}
|
|
220
|
+
this.activeSessionGenerations.delete(session.sessionGeneration);
|
|
206
221
|
await this.rpc.proxy.request.stopSession({
|
|
207
222
|
appId: session.appId,
|
|
208
223
|
sessionGeneration: session.sessionGeneration,
|
package/src/core/mode.ts
CHANGED
|
@@ -78,6 +78,16 @@ export const viewStateAtom = atom<ViewState>({
|
|
|
78
78
|
|
|
79
79
|
export const initialModeAtom = atom<AppMode | undefined>(undefined);
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Whether the current page hosts a notebook, and so connects to a kernel.
|
|
83
|
+
* False for non-notebook pages (home, gallery), which are served without a
|
|
84
|
+
* session.
|
|
85
|
+
*/
|
|
86
|
+
export function isNotebookPage(): boolean {
|
|
87
|
+
const mode = store.get(initialModeAtom);
|
|
88
|
+
return mode !== "home" && mode !== "gallery";
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export const kioskModeAtom = atom<boolean>(false);
|
|
82
92
|
|
|
83
93
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { initialModeAtom } from "@/core/mode";
|
|
5
|
+
import { store } from "@/core/state/jotai";
|
|
6
|
+
import { WebSocketState } from "../../websocket/types";
|
|
7
|
+
import { connectionAtom, waitForConnectionOpenIfNotebook } from "../connection";
|
|
8
|
+
|
|
9
|
+
const NEVER = Symbol("never");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves to NEVER if the promise does not settle within a macrotask.
|
|
13
|
+
*/
|
|
14
|
+
function settledOrNever(promise: Promise<unknown>) {
|
|
15
|
+
return Promise.race([
|
|
16
|
+
promise,
|
|
17
|
+
new Promise((resolve) => setTimeout(() => resolve(NEVER), 0)),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("waitForConnectionOpenIfNotebook", () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
store.set(connectionAtom, { state: WebSocketState.NOT_STARTED });
|
|
24
|
+
store.set(initialModeAtom, undefined);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it.each(["home", "gallery"] as const)(
|
|
28
|
+
"resolves without a connection in %s mode",
|
|
29
|
+
async (mode) => {
|
|
30
|
+
store.set(initialModeAtom, mode);
|
|
31
|
+
await expect(
|
|
32
|
+
settledOrNever(waitForConnectionOpenIfNotebook()),
|
|
33
|
+
).resolves.not.toBe(NEVER);
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
it("waits for the connection to open in edit mode", async () => {
|
|
38
|
+
store.set(initialModeAtom, "edit");
|
|
39
|
+
const promise = waitForConnectionOpenIfNotebook();
|
|
40
|
+
await expect(settledOrNever(promise)).resolves.toBe(NEVER);
|
|
41
|
+
|
|
42
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
43
|
+
await expect(settledOrNever(promise)).resolves.not.toBe(NEVER);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -32,6 +32,7 @@ vi.mock("../api", async () => {
|
|
|
32
32
|
vi.mock("../connection", () => ({
|
|
33
33
|
isConnectedAtom: { read: vi.fn(() => true) },
|
|
34
34
|
waitForConnectionOpen: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
waitForConnectionOpenIfNotebook: vi.fn().mockResolvedValue(undefined),
|
|
35
36
|
}));
|
|
36
37
|
|
|
37
38
|
describe("createNetworkRequests", () => {
|
|
@@ -121,6 +122,18 @@ describe("createNetworkRequests", () => {
|
|
|
121
122
|
expect(mockClient.GET).toHaveBeenCalledWith("/api/environment");
|
|
122
123
|
});
|
|
123
124
|
|
|
125
|
+
it("getPackageList should not require a kernel connection", async () => {
|
|
126
|
+
const { waitForConnectionOpen, waitForConnectionOpenIfNotebook } =
|
|
127
|
+
await import("../connection");
|
|
128
|
+
|
|
129
|
+
const requests = createNetworkRequests();
|
|
130
|
+
await requests.getPackageList();
|
|
131
|
+
|
|
132
|
+
expect(mockClient.GET).toHaveBeenCalledWith("/api/packages/list");
|
|
133
|
+
expect(waitForConnectionOpenIfNotebook).toHaveBeenCalled();
|
|
134
|
+
expect(waitForConnectionOpen).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
124
137
|
it("exportAsIPYNB should call the new endpoint as text", async () => {
|
|
125
138
|
const requests = createNetworkRequests();
|
|
126
139
|
await requests.exportAsIPYNB({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { atom } from "jotai";
|
|
3
|
+
import { isNotebookPage } from "../mode";
|
|
3
4
|
import { waitFor } from "../state/jotai";
|
|
4
5
|
import { type ConnectionStatus, WebSocketState } from "../websocket/types";
|
|
5
6
|
|
|
@@ -17,6 +18,20 @@ export function waitForConnectionOpen() {
|
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Waits for the kernel connection, but only on pages that open one. Non-notebook
|
|
23
|
+
* pages (home, gallery) never connect, so waiting there would hang forever.
|
|
24
|
+
*
|
|
25
|
+
* Use for requests that the marimo server can serve without a session, but that
|
|
26
|
+
* should still wait for the kernel when a notebook is open.
|
|
27
|
+
*/
|
|
28
|
+
export function waitForConnectionOpenIfNotebook() {
|
|
29
|
+
if (!isNotebookPage()) {
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
return waitForConnectionOpen();
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
export const isConnectingAtom = atom((get) => {
|
|
21
36
|
const connection = get(connectionAtom);
|
|
22
37
|
return connection.state === WebSocketState.CONNECTING;
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import { once } from "@/utils/once";
|
|
4
4
|
import { getRuntimeManager } from "../runtime/config";
|
|
5
5
|
import { API, createClientWithRuntimeManager } from "./api";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
waitForConnectionOpen,
|
|
8
|
+
waitForConnectionOpenIfNotebook,
|
|
9
|
+
} from "./connection";
|
|
7
10
|
import type { EditRequests, RunRequests } from "./types";
|
|
8
11
|
|
|
9
12
|
/**
|
|
@@ -489,12 +492,12 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
489
492
|
},
|
|
490
493
|
getPackageList: async () => {
|
|
491
494
|
// If the sidebar is already open, it may try to load before the session has been initialized
|
|
492
|
-
await
|
|
495
|
+
await waitForConnectionOpenIfNotebook();
|
|
493
496
|
return getClient().GET("/api/packages/list").then(handleResponse);
|
|
494
497
|
},
|
|
495
498
|
getDependencyTree: async () => {
|
|
496
499
|
// If the sidebar is already open, it may try to load before the session has been initialized
|
|
497
|
-
await
|
|
500
|
+
await waitForConnectionOpenIfNotebook();
|
|
498
501
|
return getClient().GET("/api/packages/tree").then(handleResponse);
|
|
499
502
|
},
|
|
500
503
|
listSecretKeys: async (request) => {
|
|
@@ -191,7 +191,9 @@ export interface EditRequests {
|
|
|
191
191
|
request: FileMoveRequest,
|
|
192
192
|
) => Promise<FileMoveResponse>;
|
|
193
193
|
sendUpdateFile: (request: FileUpdateRequest) => Promise<FileUpdateResponse>;
|
|
194
|
-
sendFileDetails: (
|
|
194
|
+
sendFileDetails: (
|
|
195
|
+
request: FileDetailsRequest,
|
|
196
|
+
) => Promise<FileDetailsResponse>;
|
|
195
197
|
// Homepage requests
|
|
196
198
|
openTutorial: (request: OpenTutorialRequest) => Promise<MarimoFile>;
|
|
197
199
|
getRecentFiles: () => Promise<RecentFilesResponse>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { FormWrapper } from "../FormPlugin";
|
|
6
|
+
|
|
7
|
+
describe("FormPlugin", () => {
|
|
8
|
+
it("submits the form on Ctrl+Enter and calls setValue", async () => {
|
|
9
|
+
const setValue = vi.fn();
|
|
10
|
+
const validate = vi.fn().mockResolvedValue(null);
|
|
11
|
+
|
|
12
|
+
render(
|
|
13
|
+
<FormWrapper
|
|
14
|
+
currentValue="old-val"
|
|
15
|
+
newValue="new-val"
|
|
16
|
+
setValue={setValue}
|
|
17
|
+
validate={validate}
|
|
18
|
+
shouldValidate={false}
|
|
19
|
+
clearOnSubmit={false}
|
|
20
|
+
submitButtonLabel="Submit"
|
|
21
|
+
bordered={true}
|
|
22
|
+
loading={false}
|
|
23
|
+
submitButtonDisabled={false}
|
|
24
|
+
showClearButton={false}
|
|
25
|
+
clearButtonLabel="Clear"
|
|
26
|
+
label={null}
|
|
27
|
+
>
|
|
28
|
+
<input data-testid="input" />
|
|
29
|
+
</FormWrapper>,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const input = screen.getByTestId("input");
|
|
33
|
+
|
|
34
|
+
// Simulate Ctrl+Enter on input
|
|
35
|
+
fireEvent.keyDown(input, { key: "Enter", ctrlKey: true });
|
|
36
|
+
|
|
37
|
+
await waitFor(() => {
|
|
38
|
+
expect(setValue).toHaveBeenCalledWith("new-val");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("submits the form on Ctrl+Enter and runs validation when shouldValidate is true", async () => {
|
|
43
|
+
const setValue = vi.fn();
|
|
44
|
+
const validate = vi.fn().mockResolvedValue("Validation failed message");
|
|
45
|
+
|
|
46
|
+
render(
|
|
47
|
+
<FormWrapper
|
|
48
|
+
currentValue="old-val"
|
|
49
|
+
newValue="new-val"
|
|
50
|
+
setValue={setValue}
|
|
51
|
+
validate={validate}
|
|
52
|
+
shouldValidate={true}
|
|
53
|
+
clearOnSubmit={false}
|
|
54
|
+
submitButtonLabel="Submit"
|
|
55
|
+
bordered={true}
|
|
56
|
+
loading={false}
|
|
57
|
+
submitButtonDisabled={false}
|
|
58
|
+
showClearButton={false}
|
|
59
|
+
clearButtonLabel="Clear"
|
|
60
|
+
label={null}
|
|
61
|
+
>
|
|
62
|
+
<input data-testid="input" />
|
|
63
|
+
</FormWrapper>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const input = screen.getByTestId("input");
|
|
67
|
+
|
|
68
|
+
// Simulate Ctrl+Enter on input
|
|
69
|
+
fireEvent.keyDown(input, { key: "Enter", ctrlKey: true });
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(validate).toHaveBeenCalledWith({ value: "new-val" });
|
|
73
|
+
// should not submit (setValue not called) due to validation failure
|
|
74
|
+
expect(setValue).not.toHaveBeenCalled();
|
|
75
|
+
// error message displayed
|
|
76
|
+
expect(screen.getByText("Validation failed message")).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("clears inputs on submission when clearOnSubmit is true", async () => {
|
|
81
|
+
const setValue = vi.fn();
|
|
82
|
+
const validate = vi.fn().mockResolvedValue(null);
|
|
83
|
+
|
|
84
|
+
let mockUIElement: (HTMLElement & { reset?: () => void }) | null = null;
|
|
85
|
+
const mockReset = vi.fn();
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<FormWrapper
|
|
89
|
+
currentValue="old-val"
|
|
90
|
+
newValue="new-val"
|
|
91
|
+
setValue={setValue}
|
|
92
|
+
validate={validate}
|
|
93
|
+
shouldValidate={false}
|
|
94
|
+
clearOnSubmit={true}
|
|
95
|
+
submitButtonLabel="Submit"
|
|
96
|
+
bordered={true}
|
|
97
|
+
loading={false}
|
|
98
|
+
submitButtonDisabled={false}
|
|
99
|
+
showClearButton={false}
|
|
100
|
+
clearButtonLabel="Clear"
|
|
101
|
+
label={null}
|
|
102
|
+
>
|
|
103
|
+
<div
|
|
104
|
+
ref={(el) => {
|
|
105
|
+
if (el && !mockUIElement) {
|
|
106
|
+
mockUIElement = document.createElement("marimo-ui-element");
|
|
107
|
+
mockUIElement.setAttribute("data-testid", "mock-ui-element");
|
|
108
|
+
mockUIElement.reset = mockReset;
|
|
109
|
+
const inputEl = document.createElement("input");
|
|
110
|
+
inputEl.type = "text";
|
|
111
|
+
mockUIElement.appendChild(inputEl);
|
|
112
|
+
el.appendChild(mockUIElement);
|
|
113
|
+
}
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
</FormWrapper>,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const input = screen.getByRole("textbox");
|
|
120
|
+
|
|
121
|
+
// Simulate Ctrl+Enter on input
|
|
122
|
+
fireEvent.keyDown(input, { key: "Enter", ctrlKey: true });
|
|
123
|
+
|
|
124
|
+
await waitFor(() => {
|
|
125
|
+
expect(setValue).toHaveBeenCalledWith("new-val");
|
|
126
|
+
expect(mockReset).toHaveBeenCalled();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("does not submit on Shift+Enter", async () => {
|
|
131
|
+
const setValue = vi.fn();
|
|
132
|
+
const validate = vi.fn().mockResolvedValue(null);
|
|
133
|
+
|
|
134
|
+
render(
|
|
135
|
+
<FormWrapper
|
|
136
|
+
currentValue="old-val"
|
|
137
|
+
newValue="new-val"
|
|
138
|
+
setValue={setValue}
|
|
139
|
+
validate={validate}
|
|
140
|
+
shouldValidate={false}
|
|
141
|
+
clearOnSubmit={false}
|
|
142
|
+
submitButtonLabel="Submit"
|
|
143
|
+
bordered={true}
|
|
144
|
+
loading={false}
|
|
145
|
+
submitButtonDisabled={false}
|
|
146
|
+
showClearButton={false}
|
|
147
|
+
clearButtonLabel="Clear"
|
|
148
|
+
label={null}
|
|
149
|
+
>
|
|
150
|
+
<input data-testid="input" />
|
|
151
|
+
</FormWrapper>,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const input = screen.getByTestId("input");
|
|
155
|
+
|
|
156
|
+
// Simulate Shift+Enter on input
|
|
157
|
+
fireEvent.keyDown(input, { key: "Enter", shiftKey: true });
|
|
158
|
+
|
|
159
|
+
expect(setValue).not.toHaveBeenCalled();
|
|
160
|
+
expect(validate).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
});
|