@trypattern/sdk 0.1.0 → 0.1.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/api.d.ts +17 -7
- package/dist/api.js +34 -8
- package/dist/app.d.ts +12 -3
- package/dist/app.js +49 -7
- package/dist/appInfo.d.ts +35 -0
- package/dist/appInfo.js +67 -0
- package/dist/appTitle.d.ts +6 -0
- package/dist/appTitle.js +3 -0
- package/dist/builtins/AppSettings.d.ts +12 -0
- package/dist/builtins/AppSettings.js +31 -0
- package/dist/builtins/Toasts.d.ts +1 -1
- package/dist/builtins/Toasts.js +2 -1
- package/dist/cli/app.js +91 -14
- package/dist/cli/publishing.d.ts +21 -0
- package/dist/cli/publishing.js +493 -0
- package/dist/clock.d.ts +6 -0
- package/dist/clock.js +31 -0
- package/dist/components/AppFrame.js +3 -3
- package/dist/components/Button.d.ts +2 -1
- package/dist/components/Button.js +6 -3
- package/dist/components/CopyButton.d.ts +11 -0
- package/dist/components/CopyButton.js +16 -0
- package/dist/components/Divider.d.ts +2 -1
- package/dist/components/Divider.js +19 -4
- package/dist/components/IconButton.js +2 -2
- package/dist/components/Link.d.ts +8 -3
- package/dist/components/Link.js +11 -2
- package/dist/components/ListRow.js +2 -2
- package/dist/components/Menu.js +2 -2
- package/dist/components/RadioGroup.d.ts +18 -0
- package/dist/components/RadioGroup.js +85 -0
- package/dist/components/SegmentedControl.js +2 -2
- package/dist/components/Sidebar.js +2 -3
- package/dist/components/SignInScreen.js +3 -3
- package/dist/components/Switch.js +2 -1
- package/dist/components/controlLook.d.ts +10 -0
- package/dist/components/controlLook.js +14 -1
- package/dist/components/icons.d.ts +4 -3
- package/dist/components/icons.js +9 -3
- package/dist/components/interaction.d.ts +1 -1
- package/dist/flows/accessFlows.js +19 -3
- package/dist/flows/browser.d.ts +3 -1
- package/dist/flows/browser.js +6 -3
- package/dist/flows/flowBrowser.d.ts +3 -1
- package/dist/flows/flowBrowser.js +4 -2
- package/dist/flows/flows.d.ts +6 -0
- package/dist/flows/flows.js +32 -5
- package/dist/hooks/appContext.d.ts +2 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -2
- package/dist/publishWire.d.ts +271 -0
- package/dist/publishWire.js +168 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.js +2 -0
- package/dist/registered.d.ts +6 -0
- package/dist/registered.js +2 -0
- package/dist/rules/checkAllFiles.js +10 -2
- package/dist/rules/lintPlugin.js +2 -0
- package/dist/rules/messages.d.ts +2 -2
- package/dist/rules/messages.js +2 -2
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +1 -0
- package/dist/server/auth.js +4 -1
- package/dist/server/createServer.d.ts +48 -3
- package/dist/server/createServer.js +25 -5
- package/dist/server/env.d.ts +2 -2
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.js +5 -1
- package/dist/server/services.d.ts +9 -0
- package/dist/server/services.js +47 -0
- package/dist/server/session.d.ts +1 -0
- package/dist/server/session.js +11 -0
- package/dist/server/settings.d.ts +49 -0
- package/dist/server/settings.js +25 -0
- package/dist/server/signInWire.d.ts +1 -0
- package/dist/server/signInWire.js +2 -1
- package/dist/serviceClients.d.ts +5 -0
- package/dist/serviceClients.js +70 -0
- package/dist/services.d.ts +53 -0
- package/dist/services.js +30 -0
- package/dist/start.js +30 -6
- package/dist/store.d.ts +5 -3
- package/dist/store.js +13 -3
- package/dist/table.d.ts +1 -1
- package/dist/vite/localWorker.d.ts +1 -1
- package/dist/vite/localWorker.js +5 -4
- package/dist/vite/projectFile.d.ts +9 -0
- package/dist/vite/projectFile.js +17 -0
- package/dist/vite/vite.d.ts +2 -1
- package/dist/vite/vite.js +10 -3
- package/package.json +5 -1
- package/presets/register.d.ts +2 -0
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Button as BaseButton } from '@base-ui/react/button';
|
|
3
3
|
import * as stylex from '@stylexjs/stylex';
|
|
4
4
|
import { colors, controls, radii } from "../theme/tokens.stylex.js";
|
|
5
|
-
import { transparentLook } from "./controlLook.js";
|
|
5
|
+
import { focusRing, transparentLook } from "./controlLook.js";
|
|
6
6
|
import { Icon } from "./Icon.js";
|
|
7
7
|
import { interact } from "./interaction.js";
|
|
8
8
|
import { Tooltip } from "./Tooltip.js";
|
|
@@ -10,7 +10,7 @@ export function IconButton({ icon, label, size = 'md', shortcut, disabled = fals
|
|
|
10
10
|
return (_jsx(Tooltip, { label: label, shortcut: shortcut, children: _jsx(BaseButton, { "aria-label": label, disabled: disabled, focusableWhenDisabled: true, onClick: () => {
|
|
11
11
|
interact({ primitive: 'IconButton', event: 'click', label });
|
|
12
12
|
onClick();
|
|
13
|
-
}, className: stylex.props(styles.button, sizes[size], transparentLook.rest, !disabled && transparentLook.hoverable, disabled && styles.disabled, style).className, children: _jsx(Icon, { name: icon, size: size }) }) }));
|
|
13
|
+
}, className: stylex.props(styles.button, sizes[size], transparentLook.rest, !disabled && transparentLook.hoverable, disabled && styles.disabled, focusRing.visible, style).className, children: _jsx(Icon, { name: icon, size: size }) }) }));
|
|
14
14
|
}
|
|
15
15
|
const styles = stylex.create({
|
|
16
16
|
button: {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { AppPath } from '../registered.ts';
|
|
3
3
|
type LinkProps = {
|
|
4
|
-
to: AppPath;
|
|
5
4
|
tone?: 'brand' | 'secondary';
|
|
6
5
|
children: ReactNode;
|
|
7
|
-
}
|
|
8
|
-
|
|
6
|
+
} & ({
|
|
7
|
+
to: AppPath;
|
|
8
|
+
href?: never;
|
|
9
|
+
} | {
|
|
10
|
+
href: string;
|
|
11
|
+
to?: never;
|
|
12
|
+
});
|
|
13
|
+
export declare function Link({ to, href, tone, children }: LinkProps): import("react").JSX.Element;
|
|
9
14
|
export {};
|
package/dist/components/Link.js
CHANGED
|
@@ -2,13 +2,22 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import * as stylex from '@stylexjs/stylex';
|
|
3
3
|
import { Link as RouterLink } from 'react-router';
|
|
4
4
|
import { colors, motion, radii } from "../theme/tokens.stylex.js";
|
|
5
|
+
import { focusRing } from "./controlLook.js";
|
|
5
6
|
import { interact, textOf } from "./interaction.js";
|
|
6
|
-
export function Link({ to, tone = 'brand', children }) {
|
|
7
|
+
export function Link({ to, href, tone = 'brand', children }) {
|
|
8
|
+
const className = stylex.props(styles.link, looks[tone], focusRing.visible).className;
|
|
9
|
+
if (href !== undefined) {
|
|
10
|
+
return (_jsx("a", { href: href, target: "_blank", rel: "noreferrer", onClick: () => interact({
|
|
11
|
+
primitive: 'Link',
|
|
12
|
+
event: 'open',
|
|
13
|
+
label: textOf(children) || href
|
|
14
|
+
}), className: className, children: children }));
|
|
15
|
+
}
|
|
7
16
|
return (_jsx(RouterLink, { to: to, onClick: () => interact({
|
|
8
17
|
primitive: 'Link',
|
|
9
18
|
event: 'navigate',
|
|
10
19
|
label: textOf(children) || to
|
|
11
|
-
}),
|
|
20
|
+
}), className: className, children: children }));
|
|
12
21
|
}
|
|
13
22
|
const styles = stylex.create({
|
|
14
23
|
link: {
|
|
@@ -3,10 +3,11 @@ import { Button as BaseButton } from '@base-ui/react/button';
|
|
|
3
3
|
import * as stylex from '@stylexjs/stylex';
|
|
4
4
|
import { Link as RouterLink } from 'react-router';
|
|
5
5
|
import { colors, controls, radii, space } from "../theme/tokens.stylex.js";
|
|
6
|
+
import { focusRing } from "./controlLook.js";
|
|
6
7
|
import { interact, textOf } from "./interaction.js";
|
|
7
8
|
export function ListRow({ to, onClick, isMuted = false, leading, trailing, children }) {
|
|
8
9
|
const content = (_jsxs(_Fragment, { children: [leading, _jsx("span", { ...stylex.props(styles.main), children: children }), trailing === undefined ? null : (_jsx("span", { ...stylex.props(styles.trailing), children: trailing }))] }));
|
|
9
|
-
const look = stylex.props(styles.row, (to !== undefined || onClick !== undefined) && styles.interactive, isMuted && styles.muted);
|
|
10
|
+
const look = stylex.props(styles.row, (to !== undefined || onClick !== undefined) && styles.interactive, isMuted && styles.muted, focusRing.visible, focusRing.inside);
|
|
10
11
|
const report = () => {
|
|
11
12
|
interact({
|
|
12
13
|
primitive: 'ListRow',
|
|
@@ -34,7 +35,6 @@ const styles = stylex.create({
|
|
|
34
35
|
color: colors.primaryForeground,
|
|
35
36
|
textAlign: 'start',
|
|
36
37
|
textDecoration: 'none',
|
|
37
|
-
outlineOffset: -2,
|
|
38
38
|
borderRadius: radii.md
|
|
39
39
|
},
|
|
40
40
|
interactive: {
|
package/dist/components/Menu.js
CHANGED
|
@@ -2,11 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Menu as BaseMenu } from '@base-ui/react/menu';
|
|
3
3
|
import * as stylex from '@stylexjs/stylex';
|
|
4
4
|
import { colors, controls, layers, space } from "../theme/tokens.stylex.js";
|
|
5
|
-
import { controlSizes, transparentLook } from "./controlLook.js";
|
|
5
|
+
import { controlSizes, focusRing, transparentLook } from "./controlLook.js";
|
|
6
6
|
import { MenuItems } from "./MenuItems.js";
|
|
7
7
|
import { Popup } from "./Popup.js";
|
|
8
8
|
export function Menu({ trigger, items, size = 'md', style }) {
|
|
9
|
-
return (_jsxs(BaseMenu.Root, { children: [_jsx(BaseMenu.Trigger, { className: (state) => stylex.props(styles.trigger, controlSizes[size], triggerPaddings[size], transparentLook.rest, transparentLook.hoverable, state.open && transparentLook.open, styles.triggerText, style).className, children: trigger }), _jsx(BaseMenu.Portal, { children: _jsx(BaseMenu.Positioner, { side: "bottom", align: "start", sideOffset: 4, className: stylex.props(styles.positioner).className, children: _jsx(BaseMenu.Popup, { render: (props, state) => (_jsx(Popup, { ...props, kind: "menu", isOpen: state.open, transitionStatus: state.transitionStatus })), children: _jsx(MenuItems, { items: items, primitive: "Menu" }) }) }) })] }));
|
|
9
|
+
return (_jsxs(BaseMenu.Root, { children: [_jsx(BaseMenu.Trigger, { className: (state) => stylex.props(styles.trigger, controlSizes[size], triggerPaddings[size], transparentLook.rest, transparentLook.hoverable, state.open && transparentLook.open, styles.triggerText, focusRing.visible, style).className, children: trigger }), _jsx(BaseMenu.Portal, { children: _jsx(BaseMenu.Positioner, { side: "bottom", align: "start", sideOffset: 4, className: stylex.props(styles.positioner).className, children: _jsx(BaseMenu.Popup, { render: (props, state) => (_jsx(Popup, { ...props, kind: "menu", isOpen: state.open, transitionStatus: state.transitionStatus })), children: _jsx(MenuItems, { items: items, primitive: "Menu" }) }) }) })] }));
|
|
10
10
|
}
|
|
11
11
|
const styles = stylex.create({
|
|
12
12
|
trigger: {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Placement } from './Placement.ts';
|
|
3
|
+
export type RadioOption<Value extends string> = {
|
|
4
|
+
value: Value;
|
|
5
|
+
label: string;
|
|
6
|
+
content?: ReactNode;
|
|
7
|
+
trailing?: ReactNode;
|
|
8
|
+
isDisabled?: boolean;
|
|
9
|
+
};
|
|
10
|
+
type RadioGroupProps<Value extends string> = {
|
|
11
|
+
label: string;
|
|
12
|
+
value: Value | null;
|
|
13
|
+
options: readonly RadioOption<Value>[];
|
|
14
|
+
onChange: (value: Value) => void;
|
|
15
|
+
style?: Placement;
|
|
16
|
+
};
|
|
17
|
+
export declare function RadioGroup<Value extends string>({ label, value, options, onChange, style }: RadioGroupProps<Value>): import("react").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Radio as BaseRadio } from '@base-ui/react/radio';
|
|
3
|
+
import { RadioGroup as BaseRadioGroup } from '@base-ui/react/radio-group';
|
|
4
|
+
import * as stylex from '@stylexjs/stylex';
|
|
5
|
+
import { borders, colors, iconSizes, motion, radii, shadows, space } from "../theme/tokens.stylex.js";
|
|
6
|
+
import { focusRing } from "./controlLook.js";
|
|
7
|
+
import { interact } from "./interaction.js";
|
|
8
|
+
import { Text } from "./Text.js";
|
|
9
|
+
export function RadioGroup({ label, value, options, onChange, style }) {
|
|
10
|
+
return (_jsx(BaseRadioGroup, { "aria-label": label, value: value, onValueChange: (next) => {
|
|
11
|
+
const chosen = options.find((option) => option.value === next);
|
|
12
|
+
if (chosen === undefined)
|
|
13
|
+
return;
|
|
14
|
+
interact({
|
|
15
|
+
primitive: 'RadioGroup',
|
|
16
|
+
event: 'change',
|
|
17
|
+
label: chosen.label
|
|
18
|
+
});
|
|
19
|
+
onChange(chosen.value);
|
|
20
|
+
}, className: stylex.props(styles.group, style).className, children: options.map((option) => (_jsxs("label", { ...stylex.props(styles.row, option.isDisabled === true && styles.rowDisabled), children: [_jsx(BaseRadio.Root, { value: option.value, disabled: option.isDisabled === true, "aria-label": option.label, className: (state) => stylex.props(styles.dot, state.checked ? styles.dotOn : styles.dotOff, focusRing.visible).className, children: _jsx(BaseRadio.Indicator, { className: stylex.props(styles.mark).className }) }), _jsx("span", { ...stylex.props(styles.content), children: option.content ?? (_jsx(Text, { tone: option.isDisabled === true ? 'tertiary' : 'primary', isTruncated: true, children: option.label })) }), option.trailing === undefined ? null : (_jsx("span", { ...stylex.props(styles.trailing), children: option.trailing }))] }, option.value))) }));
|
|
21
|
+
}
|
|
22
|
+
const styles = stylex.create({
|
|
23
|
+
group: {
|
|
24
|
+
display: 'flex',
|
|
25
|
+
flexDirection: 'column',
|
|
26
|
+
overflow: 'hidden',
|
|
27
|
+
backgroundColor: colors.componentOnContainerStrong,
|
|
28
|
+
borderRadius: radii.xl,
|
|
29
|
+
boxShadow: shadows.default
|
|
30
|
+
},
|
|
31
|
+
row: {
|
|
32
|
+
display: 'flex',
|
|
33
|
+
gap: space.md,
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
minWidth: 0,
|
|
36
|
+
paddingBlock: space.md,
|
|
37
|
+
paddingInline: space.md,
|
|
38
|
+
borderTopColor: colors.divider,
|
|
39
|
+
borderTopStyle: {
|
|
40
|
+
default: 'solid',
|
|
41
|
+
':first-child': 'none'
|
|
42
|
+
},
|
|
43
|
+
borderTopWidth: borders.hairline
|
|
44
|
+
},
|
|
45
|
+
rowDisabled: {
|
|
46
|
+
color: colors.tertiaryForeground
|
|
47
|
+
},
|
|
48
|
+
dot: {
|
|
49
|
+
display: 'grid',
|
|
50
|
+
flexShrink: 0,
|
|
51
|
+
placeItems: 'center',
|
|
52
|
+
width: iconSizes.md,
|
|
53
|
+
height: iconSizes.md,
|
|
54
|
+
padding: 0,
|
|
55
|
+
borderStyle: 'solid',
|
|
56
|
+
borderWidth: borders.thin,
|
|
57
|
+
borderRadius: radii.full,
|
|
58
|
+
transitionTimingFunction: motion.easeOut,
|
|
59
|
+
transitionDuration: motion.fast,
|
|
60
|
+
transitionProperty: 'background-color, border-color'
|
|
61
|
+
},
|
|
62
|
+
dotOff: {
|
|
63
|
+
backgroundColor: colors.componentOnContainerStrong,
|
|
64
|
+
borderColor: colors.borderStrong
|
|
65
|
+
},
|
|
66
|
+
dotOn: {
|
|
67
|
+
backgroundColor: colors.invertedSolid,
|
|
68
|
+
borderColor: colors.invertedSolid
|
|
69
|
+
},
|
|
70
|
+
mark: {
|
|
71
|
+
width: `calc(${iconSizes.md} / 2.5)`,
|
|
72
|
+
height: `calc(${iconSizes.md} / 2.5)`,
|
|
73
|
+
backgroundColor: colors.onInvertedForeground,
|
|
74
|
+
borderRadius: radii.full
|
|
75
|
+
},
|
|
76
|
+
content: {
|
|
77
|
+
flexGrow: 1,
|
|
78
|
+
minWidth: 0
|
|
79
|
+
},
|
|
80
|
+
trailing: {
|
|
81
|
+
display: 'flex',
|
|
82
|
+
flexShrink: 0,
|
|
83
|
+
alignItems: 'center'
|
|
84
|
+
}
|
|
85
|
+
});
|
|
@@ -4,13 +4,13 @@ import { RadioGroup } from '@base-ui/react/radio-group';
|
|
|
4
4
|
import * as stylex from '@stylexjs/stylex';
|
|
5
5
|
import { borders, colors, fontWeights, motion, radii } from "../theme/tokens.stylex.js";
|
|
6
6
|
import { interact } from "./interaction.js";
|
|
7
|
-
import { controlSizes } from "./controlLook.js";
|
|
7
|
+
import { controlSizes, focusRing } from "./controlLook.js";
|
|
8
8
|
export function SegmentedControl({ label, value, options, size = 'md', disabled = false, onChange, style }) {
|
|
9
9
|
const index = Math.max(0, options.findIndex((option) => option.value === value));
|
|
10
10
|
return (_jsxs(RadioGroup, { "aria-label": label, value: value, disabled: disabled, onValueChange: (next) => {
|
|
11
11
|
interact({ primitive: 'SegmentedControl', event: 'change', label });
|
|
12
12
|
onChange(next);
|
|
13
|
-
}, className: stylex.props(styles.group, style).className, children: [_jsx("span", { ...stylex.props(styles.indicator(index, options.length)) }), options.map((option) => (_jsx(Radio.Root, { value: option.value, className: (state) => stylex.props(styles.option, controlSizes[size], !disabled && styles.hoverable, state.checked && styles.optionChecked, disabled && styles.disabled).className, children: option.label }, option.value)))] }));
|
|
13
|
+
}, className: stylex.props(styles.group, style).className, children: [_jsx("span", { ...stylex.props(styles.indicator(index, options.length)) }), options.map((option) => (_jsx(Radio.Root, { value: option.value, className: (state) => stylex.props(styles.option, controlSizes[size], !disabled && styles.hoverable, state.checked && styles.optionChecked, disabled && styles.disabled, focusRing.visible).className, children: option.label }, option.value)))] }));
|
|
14
14
|
}
|
|
15
15
|
const styles = stylex.create({
|
|
16
16
|
group: {
|
|
@@ -3,6 +3,7 @@ import * as stylex from '@stylexjs/stylex';
|
|
|
3
3
|
import { NavLink } from 'react-router';
|
|
4
4
|
import { breakpoints, colors, controls, iconSizes, layers, motion, radii, shadows, space, widths } from "../theme/tokens.stylex.js";
|
|
5
5
|
import { Badge } from "./Badge.js";
|
|
6
|
+
import { focusRing } from "./controlLook.js";
|
|
6
7
|
import { Button } from "./Button.js";
|
|
7
8
|
import { Icon } from "./Icon.js";
|
|
8
9
|
import { interact } from "./interaction.js";
|
|
@@ -14,8 +15,7 @@ export function Sidebar({ workspace, workspaceMenu, links, isToggled, isAnimatin
|
|
|
14
15
|
interact({ primitive: 'AppFrame', event: 'navigate', label: link.label });
|
|
15
16
|
onFollowLink();
|
|
16
17
|
};
|
|
17
|
-
return (_jsxs("nav", { ...stylex.props(styles.sidebar, isToggled && styles.sidebarToggled, isAnimating && styles.sidebarAnimating), children: [_jsx("div", { ...stylex.props(styles.header), children: _jsx(Menu, { trigger: _jsxs(_Fragment, { children: [_jsx("span", { ...stylex.props(styles.logo), children: _jsx(Text, { variant: "caption", children: workspace.charAt(0) }) }), _jsx(Text, { variant: "label", isTruncated: true, children: workspace }), _jsx(Icon, { name: "chevronDown" })] }), items: workspaceMenu }) }), onSearch === undefined ? null : (_jsx(Button, { variant: "secondary", icon: "search", shortcut: ['mod', 'k'], isFullWidth: true, onClick: onSearch, children: "Search" })), _jsx("div", { ...stylex.props(styles.body), children: _jsx(ScrollArea, { children: _jsxs("div", { ...stylex.props(styles.group), children: [_jsx("span", { ...stylex.props(styles.groupLabel), children: _jsx(Text, { variant: "label", tone: "tertiary", children: "Workspace" }) }), links.map((item) => (_jsxs(NavLink, { onClick: () => followLink(item), to: item.to, end: item.to === '/', className: ({ isActive }) => stylex.props(styles.item, isActive && styles.itemCurrent)
|
|
18
|
-
.className ?? '', children: [_jsx(Icon, { name: item.icon }), _jsx(Text, { variant: "label", isTruncated: true, style: styles.itemLabel, children: item.label }), typeof item.count !== 'number' ? null : (_jsx(Badge, { variant: "transparent", children: item.count }))] }, item.to)))] }) }) })] }));
|
|
18
|
+
return (_jsxs("nav", { ...stylex.props(styles.sidebar, isToggled && styles.sidebarToggled, isAnimating && styles.sidebarAnimating), children: [_jsx("div", { ...stylex.props(styles.header), children: _jsx(Menu, { trigger: _jsxs(_Fragment, { children: [_jsx("span", { ...stylex.props(styles.logo), children: _jsx(Text, { variant: "caption", children: workspace.charAt(0) }) }), _jsx(Text, { variant: "label", isTruncated: true, children: workspace }), _jsx(Icon, { name: "chevronDown" })] }), items: workspaceMenu }) }), onSearch === undefined ? null : (_jsx(Button, { variant: "secondary", icon: "search", shortcut: ['mod', 'k'], isFullWidth: true, onClick: onSearch, children: "Search" })), _jsx("div", { ...stylex.props(styles.body), children: _jsx(ScrollArea, { children: _jsxs("div", { ...stylex.props(styles.group), children: [_jsx("span", { ...stylex.props(styles.groupLabel), children: _jsx(Text, { variant: "label", tone: "tertiary", children: "Workspace" }) }), links.map((item) => (_jsxs(NavLink, { onClick: () => followLink(item), to: item.to, end: item.to === '/', className: ({ isActive }) => stylex.props(styles.item, isActive && styles.itemCurrent, focusRing.visible, focusRing.inside).className ?? '', children: [_jsx(Icon, { name: item.icon }), _jsx(Text, { variant: "label", isTruncated: true, style: styles.itemLabel, children: item.label }), typeof item.count !== 'number' ? null : (_jsx(Badge, { variant: "transparent", children: item.count }))] }, item.to)))] }) }) })] }));
|
|
19
19
|
}
|
|
20
20
|
const styles = stylex.create({
|
|
21
21
|
sidebar: {
|
|
@@ -121,7 +121,6 @@ const styles = stylex.create({
|
|
|
121
121
|
':hover': colors.primaryForeground
|
|
122
122
|
},
|
|
123
123
|
textDecoration: 'none',
|
|
124
|
-
outlineOffset: 0,
|
|
125
124
|
backgroundColor: {
|
|
126
125
|
default: 'transparent',
|
|
127
126
|
':hover': colors.transparentHover
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import * as stylex from '@stylexjs/stylex';
|
|
3
|
-
import {
|
|
3
|
+
import { useModel } from "../hooks/appContext.js";
|
|
4
4
|
import { colors, controls, radii, space, widths } from "../theme/tokens.stylex.js";
|
|
5
5
|
import { Button } from "./Button.js";
|
|
6
6
|
import { Form } from "./Form.js";
|
|
7
7
|
import { Text } from "./Text.js";
|
|
8
8
|
import { TextField } from "./TextField.js";
|
|
9
9
|
export function SignInScreen() {
|
|
10
|
-
const
|
|
10
|
+
const appSettings = useModel('appSettings');
|
|
11
11
|
const signIn = useModel('signIn');
|
|
12
12
|
const isBusy = signIn.signingIn !== null;
|
|
13
|
-
return (_jsx("main", { ...stylex.props(styles.page), children: _jsxs("div", { ...stylex.props(styles.column), children: [_jsx("span", { ...stylex.props(styles.mark), children: _jsx(Text, { variant: "title", children:
|
|
13
|
+
return (_jsx("main", { ...stylex.props(styles.page), children: _jsxs("div", { ...stylex.props(styles.column), children: [_jsx("span", { ...stylex.props(styles.mark), children: _jsx(Text, { variant: "title", children: appSettings.title.charAt(0) }) }), _jsxs(Text, { as: "h1", variant: "title", tone: "primary", align: "center", children: ["Sign in to ", appSettings.title] }), signIn.step === 'choose' ? (_jsxs(_Fragment, { children: [_jsx(Button, { size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'google', onClick: signIn.continueWithGoogle, children: "Continue with Google" }), _jsxs(Form, { onSubmit: (values) => signIn.sendEmailCode({ email: values['email'] ?? '' }), children: [_jsx(TextField, { name: "email", type: "email", required: true, autoComplete: "email", placeholder: "you@example.com", label: "Email", isLabelHidden: true, size: "lg" }), _jsx(Button, { type: "submit", variant: "secondary", size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'email', children: "Email me a code" })] }), import.meta.env.DEV ? (_jsxs(Button, { variant: "tertiary", size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'dev', onClick: () => signIn.signInAsDevUser({ email: DEV_USER }), children: ["Dev: sign in as ", DEV_USER] })) : null] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { as: "p", tone: "secondary", align: "center", children: ["We sent a 6-digit code to ", signIn.email, "."] }), _jsxs(Form, { onSubmit: (values) => signIn.verifyEmailCode({ code: values['code'] ?? '' }), children: [_jsx(TextField, { name: "code", required: true, autoFocus: true, inputMode: "numeric", autoComplete: "one-time-code", pattern: "[0-9]{6}", maxLength: 6, placeholder: "123456", label: "Code", isLabelHidden: true, size: "lg" }), _jsx(Button, { type: "submit", size: "lg", disabled: isBusy, isPending: signIn.signingIn === 'email', children: "Sign in" })] }), _jsx(Button, { variant: "transparent", size: "lg", disabled: isBusy, onClick: signIn.startOver, children: "Use a different email" })] })), signIn.notice === null ? null : (_jsx(Text, { as: "p", tone: "secondary", align: "center", children: signIn.notice })), _jsx(Text, { as: "p", variant: "caption", tone: "tertiary", align: "center", children: "Sign-in by Pattern" })] }) }));
|
|
14
14
|
}
|
|
15
15
|
const DEV_USER = 'ada@acme.test';
|
|
16
16
|
const styles = stylex.create({
|
|
@@ -3,11 +3,12 @@ import { Switch as BaseSwitch } from '@base-ui/react/switch';
|
|
|
3
3
|
import * as stylex from '@stylexjs/stylex';
|
|
4
4
|
import { colors, lineHeights, motion, radii, space } from "../theme/tokens.stylex.js";
|
|
5
5
|
import { interact } from "./interaction.js";
|
|
6
|
+
import { focusRing } from "./controlLook.js";
|
|
6
7
|
export function Switch({ label, checked, disabled = false, onChange, style }) {
|
|
7
8
|
return (_jsx(BaseSwitch.Root, { "aria-label": label, checked: checked, disabled: disabled, onCheckedChange: (next) => {
|
|
8
9
|
interact({ primitive: 'Switch', event: 'change', label });
|
|
9
10
|
onChange(next);
|
|
10
|
-
}, className: (state) => stylex.props(styles.track, state.checked ? styles.on : styles.off, !disabled && (state.checked ? hovers.on : hovers.off), disabled && styles.disabled, style).className, children: _jsx(BaseSwitch.Thumb, { className: stylex.props(styles.thumb, disabled && styles.thumbDisabled).className }) }));
|
|
11
|
+
}, className: (state) => stylex.props(styles.track, state.checked ? styles.on : styles.off, !disabled && (state.checked ? hovers.on : hovers.off), disabled && styles.disabled, focusRing.visible, style).className, children: _jsx(BaseSwitch.Thumb, { className: stylex.props(styles.thumb, disabled && styles.thumbDisabled).className }) }));
|
|
11
12
|
}
|
|
12
13
|
const styles = stylex.create({
|
|
13
14
|
track: {
|
|
@@ -58,3 +58,13 @@ export declare const transparentLook: Readonly<{
|
|
|
58
58
|
readonly backgroundColor: stylex.StyleXClassNameFor<"backgroundColor", string>;
|
|
59
59
|
}>;
|
|
60
60
|
}>;
|
|
61
|
+
export declare const focusRing: Readonly<{
|
|
62
|
+
readonly visible: Readonly<{
|
|
63
|
+
readonly outlineWidth: stylex.StyleXClassNameFor<"outlineWidth", `calc(${stylex.StyleXVar<string>} + ${stylex.StyleXVar<string>})`>;
|
|
64
|
+
readonly outlineStyle: stylex.StyleXClassNameFor<"outlineStyle", "none" | "solid">;
|
|
65
|
+
readonly outlineColor: stylex.StyleXClassNameFor<"outlineColor", string>;
|
|
66
|
+
}>;
|
|
67
|
+
readonly inside: Readonly<{
|
|
68
|
+
readonly outlineOffset: stylex.StyleXClassNameFor<"outlineOffset", `calc(-1 * (${stylex.StyleXVar<string>} + ${stylex.StyleXVar<string>}))`>;
|
|
69
|
+
}>;
|
|
70
|
+
}>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as stylex from '@stylexjs/stylex';
|
|
2
|
-
import { colors, controls, fontSizes, lineHeights, motion, radii } from "../theme/tokens.stylex.js";
|
|
2
|
+
import { borders, colors, controls, fontSizes, lineHeights, motion, radii } from "../theme/tokens.stylex.js";
|
|
3
3
|
export const controlSizes = stylex.create({
|
|
4
4
|
xs: {
|
|
5
5
|
gap: controls.gapXs,
|
|
@@ -65,3 +65,16 @@ export const transparentLook = stylex.create({
|
|
|
65
65
|
backgroundColor: colors.transparentHover
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
|
+
export const focusRing = stylex.create({
|
|
69
|
+
visible: {
|
|
70
|
+
outlineWidth: `calc(${borders.thin} + ${borders.hairline})`,
|
|
71
|
+
outlineStyle: {
|
|
72
|
+
default: 'none',
|
|
73
|
+
':focus-visible': 'solid'
|
|
74
|
+
},
|
|
75
|
+
outlineColor: colors.brandBorderFocus
|
|
76
|
+
},
|
|
77
|
+
inside: {
|
|
78
|
+
outlineOffset: `calc(-1 * (${borders.thin} + ${borders.hairline}))`
|
|
79
|
+
}
|
|
80
|
+
});
|
|
@@ -6,9 +6,7 @@ type FilledDrawing = {
|
|
|
6
6
|
export type IconDrawing = string | readonly string[] | FilledDrawing;
|
|
7
7
|
export type IconName = keyof typeof builtInIcons | AppIconName;
|
|
8
8
|
declare const builtInIcons: {
|
|
9
|
-
home:
|
|
10
|
-
fill: string;
|
|
11
|
-
};
|
|
9
|
+
home: string[];
|
|
12
10
|
folder: string[];
|
|
13
11
|
settings: string[];
|
|
14
12
|
sun: {
|
|
@@ -40,6 +38,9 @@ declare const builtInIcons: {
|
|
|
40
38
|
enter: string;
|
|
41
39
|
arrowLeft: string;
|
|
42
40
|
arrowUp: string;
|
|
41
|
+
arrowUpRight: string;
|
|
42
|
+
circle: string;
|
|
43
|
+
copy: string[];
|
|
43
44
|
arrowDown: string;
|
|
44
45
|
logOut: string[];
|
|
45
46
|
};
|
package/dist/components/icons.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const builtInIcons = {
|
|
2
|
-
home:
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
home: [
|
|
3
|
+
'M14.25 20.25H18.25C19.3546 20.25 20.25 19.3546 20.25 18.25V9.90496C20.25 9.32928 20.0019 8.78149 19.5702 8.40225L13.3202 2.91021C12.5644 2.24604 11.4356 2.24604 10.6798 2.91021L4.42977 8.40225C3.99813 8.78149 3.75 9.32928 3.75 9.90496V18.25C3.75 19.3546 4.64543 20.25 5.75 20.25H9.75V16.75C9.75 15.6454 10.6454 14.75 11.75 14.75H12.25C13.3546 14.75 14.25 15.6454 14.25 16.75V20.25Z'
|
|
4
|
+
],
|
|
5
5
|
folder: [
|
|
6
6
|
'M9.13202 3.75H4.75C3.64543 3.75 2.75 4.64543 2.75 5.75V17.25C2.75 18.3546 3.64543 19.25 4.75 19.25H19.25C20.3546 19.25 21.25 18.3546 21.25 17.25V7.75C21.25 6.64543 20.3546 5.75 19.25 5.75H12.8124C12.2915 5.75 11.7911 5.54674 11.4177 5.18345L10.5267 4.31655C10.1534 3.95326 9.65297 3.75 9.13202 3.75Z',
|
|
7
7
|
'M2.75 12.75V11.75C2.75 10.6454 3.64543 9.75 4.75 9.75H19.25C20.3546 9.75 21.25 10.6454 21.25 11.75V12.75'
|
|
@@ -77,6 +77,12 @@ const builtInIcons = {
|
|
|
77
77
|
enter: 'M19 5v6.5a3 3 0 0 1-3 3H5.5M9 10.5l-4 4 4 4',
|
|
78
78
|
arrowLeft: 'M19 12H5M11 6l-6 6 6 6',
|
|
79
79
|
arrowUp: 'M12 19V5M6 11l6-6 6 6',
|
|
80
|
+
arrowUpRight: 'M7 17L17 7M8 7h9v9',
|
|
81
|
+
circle: 'M3.5 12a8.5 8.5 0 1 0 17 0a8.5 8.5 0 1 0 -17 0',
|
|
82
|
+
copy: [
|
|
83
|
+
'M10 9h8.5a1.5 1.5 0 0 1 1.5 1.5V19a1.5 1.5 0 0 1-1.5 1.5H10A1.5 1.5 0 0 1 8.5 19v-8.5A1.5 1.5 0 0 1 10 9z',
|
|
84
|
+
'M15.5 9V5.5A1.5 1.5 0 0 0 14 4H5.5A1.5 1.5 0 0 0 4 5.5V14a1.5 1.5 0 0 0 1.5 1.5H8.5'
|
|
85
|
+
],
|
|
80
86
|
arrowDown: 'M12 5v14M6 13l6 6 6-6',
|
|
81
87
|
logOut: [
|
|
82
88
|
'M14 4.5H7.5a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2H14',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
export type Interaction = {
|
|
3
|
-
primitive: 'AppFrame' | 'Button' | 'IconButton' | 'Link' | 'ListRow' | 'Select' | 'Switch' | 'SegmentedControl' | 'Menu' | 'ContextMenu' | 'CommandMenu' | 'TextField' | 'Form' | 'Dialog';
|
|
3
|
+
primitive: 'AppFrame' | 'Button' | 'IconButton' | 'Link' | 'ListRow' | 'Select' | 'Switch' | 'SegmentedControl' | 'Menu' | 'ContextMenu' | 'CommandMenu' | 'TextField' | 'Form' | 'Dialog' | 'RadioGroup';
|
|
4
4
|
event: 'click' | 'change' | 'select' | 'open' | 'close' | 'submit' | 'navigate';
|
|
5
5
|
label: string;
|
|
6
6
|
page: string | null;
|
|
@@ -31,16 +31,27 @@ function idsOf(rows) {
|
|
|
31
31
|
return rows.map((row) => row.id);
|
|
32
32
|
}
|
|
33
33
|
export function accessFlows({ App }) {
|
|
34
|
-
if (inject('patternVisualFlows') !== undefined)
|
|
34
|
+
if (inject('patternVisualFlows') !== undefined) {
|
|
35
|
+
test.skip('Access flows run headless, in app test', () => undefined);
|
|
35
36
|
return;
|
|
36
|
-
|
|
37
|
+
}
|
|
38
|
+
const { tables, routes, services, memory } = App.definition;
|
|
37
39
|
const seed = (inject('patternSeed') ?? {});
|
|
38
40
|
const admins = inject('patternAdmins') ?? [];
|
|
41
|
+
const title = inject('patternTitle') ?? '';
|
|
39
42
|
const [admin] = admins;
|
|
40
43
|
async function started(signedInAs) {
|
|
41
44
|
const gate = new Gate();
|
|
42
45
|
const app = new App(createMemoryRouter(routeObjects(routes), { initialEntries: ['/'] }), {
|
|
43
|
-
api: memoryApi(tables, seed, {
|
|
46
|
+
api: memoryApi(tables, seed, {
|
|
47
|
+
gate,
|
|
48
|
+
signedInAs,
|
|
49
|
+
admins,
|
|
50
|
+
title,
|
|
51
|
+
world: structuredClone(inject('patternWorld') ?? {}),
|
|
52
|
+
...(services === undefined ? {} : { services }),
|
|
53
|
+
...(memory === undefined ? {} : { memory })
|
|
54
|
+
}),
|
|
44
55
|
cache: Cache.memory(),
|
|
45
56
|
gate
|
|
46
57
|
});
|
|
@@ -63,6 +74,11 @@ export function accessFlows({ App }) {
|
|
|
63
74
|
: []
|
|
64
75
|
};
|
|
65
76
|
}
|
|
77
|
+
if (Object.keys(tables).length === 0) {
|
|
78
|
+
test('Access: schema.ts has no tables yet, so there are no rules to check', () => {
|
|
79
|
+
expect(Object.keys(tables)).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
66
82
|
for (const table of Object.values(tables)) {
|
|
67
83
|
const name = table.name;
|
|
68
84
|
const rows = seededRows(seed, name);
|
package/dist/flows/browser.d.ts
CHANGED
|
@@ -24,9 +24,11 @@ export type AppPage = {
|
|
|
24
24
|
snapshot: () => Promise<unknown>;
|
|
25
25
|
hold: (call: string) => Promise<void>;
|
|
26
26
|
release: (call: string) => Promise<void>;
|
|
27
|
+
tick: () => Promise<void>;
|
|
28
|
+
world: (patch: object) => Promise<void>;
|
|
27
29
|
reload: (path: string) => Promise<void>;
|
|
28
30
|
screenshot: (file: string) => Promise<void>;
|
|
29
31
|
captureAt: (viewport: ViewportName) => Promise<string>;
|
|
30
32
|
close: () => Promise<void>;
|
|
31
33
|
};
|
|
32
|
-
export declare function openApp(browser: Browser, baseUrl: string, path: string, viewport: ViewportName, signedInAs: string | null, seed?: object, admins?: readonly string[]): Promise<AppPage>;
|
|
34
|
+
export declare function openApp(browser: Browser, baseUrl: string, path: string, viewport: ViewportName, signedInAs: string | null, seed?: object, admins?: readonly string[], world?: unknown): Promise<AppPage>;
|
package/dist/flows/browser.js
CHANGED
|
@@ -81,7 +81,7 @@ function recordProblems(page, problems) {
|
|
|
81
81
|
}
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
|
-
export async function openApp(browser, baseUrl, path, viewport, signedInAs, seed, admins = []) {
|
|
84
|
+
export async function openApp(browser, baseUrl, path, viewport, signedInAs, seed, admins = [], world = {}) {
|
|
85
85
|
const { width, height, deviceScaleFactor } = viewports[viewport];
|
|
86
86
|
const context = await browser.newContext({
|
|
87
87
|
viewport: { width, height },
|
|
@@ -99,9 +99,10 @@ export async function openApp(browser, baseUrl, path, viewport, signedInAs, seed
|
|
|
99
99
|
await context.addInitScript((value) => {
|
|
100
100
|
Object.assign(globalThis, {
|
|
101
101
|
patternSeed: value.seed,
|
|
102
|
-
patternAdmins: value.admins
|
|
102
|
+
patternAdmins: value.admins,
|
|
103
|
+
patternWorld: value.world
|
|
103
104
|
});
|
|
104
|
-
}, { seed, admins });
|
|
105
|
+
}, { seed, admins, world });
|
|
105
106
|
}
|
|
106
107
|
const page = await context.newPage();
|
|
107
108
|
const problems = [];
|
|
@@ -142,6 +143,8 @@ export async function openApp(browser, baseUrl, path, viewport, signedInAs, seed
|
|
|
142
143
|
snapshot,
|
|
143
144
|
hold: (call) => page.evaluate((held) => globalThis.pattern.hold(held), call),
|
|
144
145
|
release: (call) => page.evaluate((released) => globalThis.pattern.release(released), call),
|
|
146
|
+
tick: () => page.evaluate(() => globalThis.pattern.tick()),
|
|
147
|
+
world: (patch) => page.evaluate((changed) => globalThis.pattern.world(changed), patch),
|
|
145
148
|
reload: load,
|
|
146
149
|
screenshot: async (file) => {
|
|
147
150
|
await settle();
|
|
@@ -11,11 +11,13 @@ declare module 'vitest' {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
export declare function closeFlowBrowser(): Promise<void>;
|
|
14
|
-
export declare function openFlowPage(settings: VisualFlowSettings, viewport: ViewportName, startAt: string, signedInAs: string | null, seed: object, admins: readonly string[]): Promise<{
|
|
14
|
+
export declare function openFlowPage(settings: VisualFlowSettings, viewport: ViewportName, startAt: string, signedInAs: string | null, seed: object, admins: readonly string[], world: unknown): Promise<{
|
|
15
15
|
send: (command: unknown) => Promise<void>;
|
|
16
16
|
snapshot: () => Promise<unknown>;
|
|
17
17
|
hold: (call: string) => Promise<void>;
|
|
18
18
|
release: (call: string) => Promise<void>;
|
|
19
|
+
tick: () => Promise<void>;
|
|
20
|
+
world: (patch: object) => Promise<void>;
|
|
19
21
|
reload: (path: string) => Promise<void>;
|
|
20
22
|
screenshot: (name: string) => Promise<void>;
|
|
21
23
|
problems: string[];
|
|
@@ -5,14 +5,16 @@ export async function closeFlowBrowser() {
|
|
|
5
5
|
browser = undefined;
|
|
6
6
|
await (await connected)?.close();
|
|
7
7
|
}
|
|
8
|
-
export async function openFlowPage(settings, viewport, startAt, signedInAs, seed, admins) {
|
|
8
|
+
export async function openFlowPage(settings, viewport, startAt, signedInAs, seed, admins, world) {
|
|
9
9
|
browser ??= connectChrome(settings.wsEndpoint);
|
|
10
|
-
const app = await openApp(await browser, settings.url, startAt, viewport, signedInAs, seed, admins);
|
|
10
|
+
const app = await openApp(await browser, settings.url, startAt, viewport, signedInAs, seed, admins, world);
|
|
11
11
|
return {
|
|
12
12
|
send: app.dispatch,
|
|
13
13
|
snapshot: app.snapshot,
|
|
14
14
|
hold: app.hold,
|
|
15
15
|
release: app.release,
|
|
16
|
+
tick: app.tick,
|
|
17
|
+
world: app.world,
|
|
16
18
|
reload: app.reload,
|
|
17
19
|
screenshot: (name) => app.screenshot(`${settings.directory}/${name}-${viewport}.png`),
|
|
18
20
|
problems: app.problems,
|
package/dist/flows/flows.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ declare module 'vitest' {
|
|
|
8
8
|
interface ProvidedContext {
|
|
9
9
|
patternSeed?: Seed<Tables>;
|
|
10
10
|
patternAdmins?: readonly string[];
|
|
11
|
+
patternTitle?: string;
|
|
12
|
+
patternWorld?: unknown;
|
|
11
13
|
}
|
|
12
14
|
}
|
|
13
15
|
type Expected<Value> = Value extends readonly (infer Item)[] ? readonly Expected<Item>[] : Value extends object ? {
|
|
@@ -25,6 +27,10 @@ export type FlowStep<Command, Snapshot, Call extends string> = {
|
|
|
25
27
|
release: Call;
|
|
26
28
|
} | {
|
|
27
29
|
restart: true;
|
|
30
|
+
} | {
|
|
31
|
+
tick: true;
|
|
32
|
+
} | {
|
|
33
|
+
world: object;
|
|
28
34
|
};
|
|
29
35
|
export type FlowOptions<Call extends string> = {
|
|
30
36
|
startAt?: string;
|
package/dist/flows/flows.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createMemoryRouter } from 'react-router';
|
|
|
2
2
|
import { afterAll, expect, inject, test } from 'vitest';
|
|
3
3
|
import { apiCalls, memoryApi } from "../api.js";
|
|
4
4
|
import { Cache } from "../cache.js";
|
|
5
|
+
import { manualClock } from "../clock.js";
|
|
5
6
|
import { Gate } from "../gate.js";
|
|
6
7
|
import { routeObjects } from "../routes.js";
|
|
7
8
|
import { screenshotNameProblem } from "./screenshotName.js";
|
|
@@ -47,6 +48,12 @@ async function runSteps(name, steps, run) {
|
|
|
47
48
|
else if ('restart' in step) {
|
|
48
49
|
await run.restart();
|
|
49
50
|
}
|
|
51
|
+
else if ('tick' in step) {
|
|
52
|
+
await run.tick();
|
|
53
|
+
}
|
|
54
|
+
else if ('world' in step) {
|
|
55
|
+
await run.world(step.world);
|
|
56
|
+
}
|
|
50
57
|
else {
|
|
51
58
|
expect(await run.snapshot(), `step ${index + 1} of "${name}": expected ${JSON.stringify(step.expect)}`).toMatchObject(step.expect);
|
|
52
59
|
}
|
|
@@ -61,18 +68,30 @@ function withTestParams(path, failing, held) {
|
|
|
61
68
|
return `${path}?${params.toString()}`;
|
|
62
69
|
}
|
|
63
70
|
export function defineFlow({ App }) {
|
|
64
|
-
const { tables, routes } = App.definition;
|
|
71
|
+
const { tables, routes, services, memory } = App.definition;
|
|
65
72
|
const seed = (inject('patternSeed') ?? {});
|
|
66
73
|
const admins = inject('patternAdmins') ?? [];
|
|
67
|
-
const
|
|
74
|
+
const title = inject('patternTitle') ?? '';
|
|
75
|
+
const startingWorld = inject('patternWorld') ?? {};
|
|
76
|
+
const calls = apiCalls(tables, services);
|
|
68
77
|
const router = (path) => createMemoryRouter(routeObjects(routes), { initialEntries: [path] });
|
|
69
78
|
function headless(name, steps, startAt, failing, signedInAs) {
|
|
70
79
|
test(name, async () => {
|
|
71
80
|
const gate = new Gate(failing);
|
|
81
|
+
const world = structuredClone(startingWorld);
|
|
72
82
|
const deps = {
|
|
73
|
-
api: memoryApi(tables, seed, {
|
|
83
|
+
api: memoryApi(tables, seed, {
|
|
84
|
+
gate,
|
|
85
|
+
signedInAs,
|
|
86
|
+
admins,
|
|
87
|
+
title,
|
|
88
|
+
world,
|
|
89
|
+
...(services === undefined ? {} : { services }),
|
|
90
|
+
...(memory === undefined ? {} : { memory })
|
|
91
|
+
}),
|
|
74
92
|
cache: Cache.memory(),
|
|
75
|
-
gate
|
|
93
|
+
gate,
|
|
94
|
+
clock: manualClock()
|
|
76
95
|
};
|
|
77
96
|
let app = new App(router(startAt), deps);
|
|
78
97
|
await runSteps(name, steps, {
|
|
@@ -86,6 +105,12 @@ export function defineFlow({ App }) {
|
|
|
86
105
|
release: async (call) => gate.release(call),
|
|
87
106
|
restart: async () => {
|
|
88
107
|
app = new App(router(app.getSnapshot().path), deps);
|
|
108
|
+
},
|
|
109
|
+
tick: async () => {
|
|
110
|
+
app.tick();
|
|
111
|
+
},
|
|
112
|
+
world: async (patch) => {
|
|
113
|
+
Object.assign(Object(world), patch);
|
|
89
114
|
}
|
|
90
115
|
});
|
|
91
116
|
await app.whenIdle();
|
|
@@ -109,7 +134,7 @@ export function defineFlow({ App }) {
|
|
|
109
134
|
test(`${name} [${viewport}]`, async () => {
|
|
110
135
|
const { openFlowPage } = await import("./flowBrowser.js");
|
|
111
136
|
const held = new Set();
|
|
112
|
-
const page = await openFlowPage(visual, viewport, withTestParams(startAt, failing, held), signedInAs, seed, admins);
|
|
137
|
+
const page = await openFlowPage(visual, viewport, withTestParams(startAt, failing, held), signedInAs, seed, admins, startingWorld);
|
|
113
138
|
let stepFailure;
|
|
114
139
|
try {
|
|
115
140
|
await runSteps(name, steps, {
|
|
@@ -124,6 +149,8 @@ export function defineFlow({ App }) {
|
|
|
124
149
|
held.delete(call);
|
|
125
150
|
await page.release(call);
|
|
126
151
|
},
|
|
152
|
+
tick: page.tick,
|
|
153
|
+
world: page.world,
|
|
127
154
|
restart: async () => {
|
|
128
155
|
const snapshot = await page.snapshot();
|
|
129
156
|
const path = typeof snapshot === 'object' && snapshot !== null
|