diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 3c2d7762378d..9d92b30e33cf 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -167,11 +167,6 @@ "count": 1 } }, - "static/app/components/core/useScrollLock.spec.tsx": { - "@sentry/require-fake-timer-cleanup": { - "count": 1 - } - }, "static/app/components/customCommitsResolutionModal.tsx": { "@sentry/scraps/prefer-primitives": { "count": 1 diff --git a/oxlint.config.ts b/oxlint.config.ts index 4fde4e7b82fa..fc01e7637af0 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -1654,10 +1654,9 @@ const config = defineConfig({ 'import/no-relative-parent-imports': 'off', }, }, - // Scraps is its own component library rather than ordinary app code, and a - // handful of its internal imports are deliberately parent-relative. + // Legacy app core components still use parent-relative imports. { - files: coreComponentFiles, + files: ['static/app/components/core/**/*.{js,mjs,ts,jsx,tsx}'], rules: { 'import/no-relative-parent-imports': 'off', }, @@ -1817,7 +1816,6 @@ const config = defineConfig({ 'static/packages/icons/test/**/*.{ts,tsx,mjs}', ], rules: { - 'import/no-relative-parent-imports': 'off', 'import/no-nodejs-modules': 'off', 'no-restricted-imports': [ 'error', diff --git a/package.json b/package.json index 889b00894066..b63e6df233f6 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,6 @@ "@types/gtag.js": "^0.0.12", "@types/jest": "30.0.0", "@types/react": "19.3.0", - "@types/react-dom": "19.3.0", "@types/trusted-types": "^2.0.7", "compression-webpack-plugin": "12.0.0", "core-js": "3.45.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 599de0202959..29daee01b1d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -251,9 +251,6 @@ importers: '@types/react': specifier: 19.3.0 version: 19.3.0 - '@types/react-dom': - specifier: 19.3.0 - version: 19.3.0(@types/react@19.3.0) '@types/trusted-types': specifier: ^2.0.7 version: 2.0.7 @@ -1190,12 +1187,33 @@ importers: '@emotion/styled': specifier: ^11.14.0 version: 11.14.0(@emotion/react@11.14.0(@types/react@19.3.0)(react@19.3.0)(supports-color@8.1.1))(@types/react@19.3.0)(react@19.3.0)(supports-color@8.1.1) + '@react-aria/i18n': + specifier: 3.13.1 + version: 3.13.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@react-aria/slider': + specifier: 3.9.1 + version: 3.9.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@react-aria/utils': specifier: 3.34.1 version: 3.34.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@react-aria/visually-hidden': + specifier: 3.9.1 + version: 3.9.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@react-stately/slider': + specifier: 3.8.1 + version: 3.8.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@sentry/icons': + specifier: workspace:* + version: link:../icons + '@sentry/react': + specifier: 11.6.0 + version: 11.6.0(react-router@6.30.6(react@19.3.0))(react@19.3.0) '@types/react': specifier: 19.3.0 version: 19.3.0 + framer-motion: + specifier: 12.38.0 + version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) react: specifier: ^19.0.0 version: 19.3.0 @@ -1230,12 +1248,18 @@ importers: '@testing-library/react': specifier: 16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@testing-library/user-event': + specifier: 14.6.7 + version: 14.6.7(@testing-library/dom@10.4.1) '@types/jest': specifier: 30.0.0 version: 30.0.0 '@types/node': specifier: 26.6.4 version: 26.6.4 + '@types/react-dom': + specifier: 19.3.0 + version: 19.3.0(@types/react@19.3.0) expect-type: specifier: 1.4.0 version: 1.4.0 diff --git a/static/app/actionCreators/modal.tsx b/static/app/actionCreators/modal.tsx index 54fe8e8a0bee..a845c1a63e43 100644 --- a/static/app/actionCreators/modal.tsx +++ b/static/app/actionCreators/modal.tsx @@ -1,4 +1,5 @@ import type {ModalTypes} from '@sentry/scraps/modal'; +import type {Theme} from '@sentry/scraps/theme'; import type { CommandPaletteState, @@ -26,7 +27,6 @@ import type {IssueOwnership} from 'sentry/types/group'; import type {MissingMember, Organization, OrgRole} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; -import type {Theme} from 'sentry/utils/theme'; import {DisplayType} from 'sentry/views/dashboards/types'; import type {AttributeBreakdownViewerModalOptions} from 'sentry/views/explore/components/attributeBreakdowns/attributeBreakdownViewerModal'; diff --git a/static/app/chartcuterie/config.tsx b/static/app/chartcuterie/config.tsx index e3f0745fcb47..acfc5737a717 100644 --- a/static/app/chartcuterie/config.tsx +++ b/static/app/chartcuterie/config.tsx @@ -8,7 +8,7 @@ * into the configuration file loaded by the service. */ -import {lightTheme} from 'sentry/utils/theme/theme'; +import {lightTheme} from '@sentry/scraps/theme'; import {makeDashboardsWidgetCharts} from './dashboardsWidget'; import {makeDiscoverCharts} from './discover'; diff --git a/static/app/components/charts/chartLegend.tsx b/static/app/components/charts/chartLegend.tsx index fe5d414b51cd..d57bbb4bf822 100644 --- a/static/app/components/charts/chartLegend.tsx +++ b/static/app/components/charts/chartLegend.tsx @@ -8,9 +8,9 @@ import {CompactSelect} from '@sentry/scraps/compactSelect'; import {Flex} from '@sentry/scraps/layout'; import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import {Text} from '@sentry/scraps/text'; +import type {SpaceSize} from '@sentry/scraps/theme'; import {t} from 'sentry/locale'; -import type {SpaceSize} from 'sentry/utils/theme'; import {useDimensions} from 'sentry/utils/useDimensions'; import {LegendCheckbox} from './components/legendCheckbox'; diff --git a/static/app/components/commandPalette/ui/commandPalette.tsx b/static/app/components/commandPalette/ui/commandPalette.tsx index 0c13069c6206..bbbead5a0004 100644 --- a/static/app/components/commandPalette/ui/commandPalette.tsx +++ b/static/app/components/commandPalette/ui/commandPalette.tsx @@ -29,6 +29,7 @@ import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {InnerWrap} from '@sentry/scraps/menuListItem'; import type {MenuListItemProps} from '@sentry/scraps/menuListItem'; import {Text} from '@sentry/scraps/text'; +import type {Theme} from '@sentry/scraps/theme'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import type {CMDKActionData} from 'sentry/components/commandPalette/ui/cmdk'; @@ -48,7 +49,6 @@ import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants'; import {t} from 'sentry/locale'; import {fzf} from 'sentry/utils/search/fzf'; -import type {Theme} from 'sentry/utils/theme'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useFeedbackForm} from 'sentry/utils/useFeedbackForm'; import {useNavigate} from 'sentry/utils/useNavigate'; diff --git a/static/app/components/core/alert/alert.tsx b/static/app/components/core/alert/alert.tsx index 431d5eb0bc3f..c7d8a8d61327 100644 --- a/static/app/components/core/alert/alert.tsx +++ b/static/app/components/core/alert/alert.tsx @@ -12,11 +12,11 @@ import type {DistributedOmit} from 'type-fest'; import {Button, type ButtonProps} from '@sentry/scraps/button'; import {Container, Flex} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; +import type {AlertVariant} from '@sentry/scraps/theme'; import {useTranslation} from '@sentry/scraps/translation/useTranslation'; import {defined} from 'sentry/utils/defined'; import {PanelProvider} from 'sentry/utils/panelProvider'; -import type {AlertVariant} from 'sentry/utils/theme'; import {unreachable} from 'sentry/utils/unreachable'; export interface AlertProps extends React.HTMLAttributes { diff --git a/static/app/components/core/badge/featureBadge.tsx b/static/app/components/core/badge/featureBadge.tsx index 676b7057b470..1d8fa76c60ea 100644 --- a/static/app/components/core/badge/featureBadge.tsx +++ b/static/app/components/core/badge/featureBadge.tsx @@ -3,12 +3,11 @@ import {IconBroadcast} from '@sentry/icons/broadcast'; import {IconBug} from '@sentry/icons/bug'; import {IconLab} from '@sentry/icons/lab'; +import type {TagVariant} from '@sentry/scraps/theme'; import {Tooltip, type TooltipProps} from '@sentry/scraps/tooltip'; import {useTranslation} from '@sentry/scraps/translation/useTranslation'; import {useIsInsideInteractiveElement} from '@sentry/scraps/useIsInsideInteractiveElement'; -import type {TagVariant} from 'sentry/utils/theme'; - import {Tag} from './tag'; function useDefaultTitle(type: FeatureBadgeProps['type']) { diff --git a/static/app/components/core/badge/tag.tsx b/static/app/components/core/badge/tag.tsx index 0eb1a55b3497..67ed5aad4c0c 100644 --- a/static/app/components/core/badge/tag.tsx +++ b/static/app/components/core/badge/tag.tsx @@ -4,9 +4,9 @@ import {IconClose} from '@sentry/icons/close'; import {IconDefaultsProvider} from '@sentry/icons/useIconDefaults'; import {Button} from '@sentry/scraps/button'; +import type {TagVariant} from '@sentry/scraps/theme'; import {useTranslation} from '@sentry/scraps/translation/useTranslation'; -import type {TagVariant} from 'sentry/utils/theme'; import {unreachable} from 'sentry/utils/unreachable'; export interface TagProps extends React.HTMLAttributes { diff --git a/static/app/components/core/button/styles.tsx b/static/app/components/core/button/styles.tsx index c96a2afe2b2e..ff54d94ecfdd 100644 --- a/static/app/components/core/button/styles.tsx +++ b/static/app/components/core/button/styles.tsx @@ -1,8 +1,7 @@ import {type SVGIconProps} from '@sentry/icons/svgIcon'; import type {ButtonProps} from '@sentry/scraps/button'; - -import type {StrictCSSObject, Theme} from 'sentry/utils/theme'; +import type {StrictCSSObject, Theme} from '@sentry/scraps/theme'; import { type ButtonVariant, diff --git a/static/app/components/core/checkbox/checkbox.tsx b/static/app/components/core/checkbox/checkbox.tsx index f4c4ad2005f6..a963232b001c 100644 --- a/static/app/components/core/checkbox/checkbox.tsx +++ b/static/app/components/core/checkbox/checkbox.tsx @@ -4,10 +4,9 @@ import styled from '@emotion/styled'; import {mergeRefs} from '@react-aria/utils'; import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; +import type {FormSize} from '@sentry/scraps/theme'; import {Tooltip, useDisabledTooltip, type TooltipProps} from '@sentry/scraps/tooltip'; -import type {FormSize} from 'sentry/utils/theme'; - type CheckboxConfig = { borderRadius: string; box: string; diff --git a/static/app/components/core/code/codeBlock.tsx b/static/app/components/core/code/codeBlock.tsx index dd807bd514f3..e22910f69167 100644 --- a/static/app/components/core/code/codeBlock.tsx +++ b/static/app/components/core/code/codeBlock.tsx @@ -6,10 +6,9 @@ import Prism from 'prismjs'; import {Button} from '@sentry/scraps/button'; import {Container} from '@sentry/scraps/layout'; +import {darkTheme} from '@sentry/scraps/theme'; import {useTranslation} from '@sentry/scraps/translation/useTranslation'; -import {darkTheme} from 'sentry/utils/theme/theme'; - import {getPrismLanguage, loadPrismLanguage} from './prism'; interface CodeBlockProps { diff --git a/static/app/components/core/compactSelect/control.tsx b/static/app/components/core/compactSelect/control.tsx index adbc81180a95..ef21af273fb4 100644 --- a/static/app/components/core/compactSelect/control.tsx +++ b/static/app/components/core/compactSelect/control.tsx @@ -15,11 +15,11 @@ import {Button} from '@sentry/scraps/button'; import {InputGroup} from '@sentry/scraps/input'; import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {OverlayTrigger, type TriggerProps} from '@sentry/scraps/overlayTrigger'; +import type {FormSize} from '@sentry/scraps/theme'; import {useTranslation} from '@sentry/scraps/translation/useTranslation'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {Overlay, PositionWrapper} from 'sentry/components/overlay'; -import type {FormSize} from 'sentry/utils/theme'; import type {UseOverlayProps} from 'sentry/utils/useOverlay'; import {useOverlay} from 'sentry/utils/useOverlay'; import {usePrevious} from 'sentry/utils/usePrevious'; diff --git a/static/app/components/core/compactSelect/controlContext.tsx b/static/app/components/core/compactSelect/controlContext.tsx index a5a3981a902c..b50af97f36e3 100644 --- a/static/app/components/core/compactSelect/controlContext.tsx +++ b/static/app/components/core/compactSelect/controlContext.tsx @@ -1,7 +1,7 @@ import {createContext} from 'react'; import type {OverlayTriggerState} from '@react-stately/overlays'; -import type {FormSize} from 'sentry/utils/theme'; +import type {FormSize} from '@sentry/scraps/theme'; import type {SearchMatchResult, SelectKey, SelectOptionWithKey} from './types'; diff --git a/static/app/components/core/compactSelect/gridList/option.tsx b/static/app/components/core/compactSelect/gridList/option.tsx index 6b23d2723c0d..c07fe7bc2fcb 100644 --- a/static/app/components/core/compactSelect/gridList/option.tsx +++ b/static/app/components/core/compactSelect/gridList/option.tsx @@ -18,8 +18,7 @@ import { MenuListItem, type MenuListItemProps, } from '@sentry/scraps/menuListItem'; - -import type {FormSize} from 'sentry/utils/theme'; +import type {FormSize} from '@sentry/scraps/theme'; export interface GridListOptionProps< T extends ListItemBase, diff --git a/static/app/components/core/compactSelect/list.tsx b/static/app/components/core/compactSelect/list.tsx index 846d5316672b..3342f8ff7c56 100644 --- a/static/app/components/core/compactSelect/list.tsx +++ b/static/app/components/core/compactSelect/list.tsx @@ -5,8 +5,9 @@ import type {AriaListBoxOptions} from '@react-aria/listbox'; import type {ListProps} from '@react-stately/list'; import {useListState} from '@react-stately/list'; +import type {FormSize} from '@sentry/scraps/theme'; + import {defined} from 'sentry/utils/defined'; -import type {FormSize} from 'sentry/utils/theme'; import {ControlContext} from './controlContext'; import {GridList} from './gridList'; diff --git a/static/app/components/core/compactSelect/listBox/index.tsx b/static/app/components/core/compactSelect/listBox/index.tsx index fc54cdde92f2..8ee03ab2bb41 100644 --- a/static/app/components/core/compactSelect/listBox/index.tsx +++ b/static/app/components/core/compactSelect/listBox/index.tsx @@ -15,10 +15,9 @@ import { import type {SelectKey, ListItemBase} from '@sentry/scraps/compactSelect/types'; import {useVirtualizedItems} from '@sentry/scraps/compactSelect/useVirtualizedItems'; import {Container} from '@sentry/scraps/layout'; +import type {FormSize} from '@sentry/scraps/theme'; import {useTranslation} from '@sentry/scraps/translation/useTranslation'; -import type {FormSize} from 'sentry/utils/theme'; - import {ListBoxOption} from './option'; import {ListBoxSection} from './section'; diff --git a/static/app/components/core/compactSelect/useVirtualizedItems.tsx b/static/app/components/core/compactSelect/useVirtualizedItems.tsx index 85aeadd0cbaa..1d628d150419 100644 --- a/static/app/components/core/compactSelect/useVirtualizedItems.tsx +++ b/static/app/components/core/compactSelect/useVirtualizedItems.tsx @@ -1,8 +1,9 @@ import {useRef} from 'react'; import type {Node} from '@react-types/shared'; +import type {FormSize} from '@sentry/scraps/theme'; + import {useVirtualRows} from 'sentry/components/tables/useVirtualRows'; -import type {FormSize} from 'sentry/utils/theme'; const heightEstimations = { sm: {regular: 32, large: 49}, diff --git a/static/app/components/core/composer/styles.tsx b/static/app/components/core/composer/styles.tsx index f0a921c60c73..7caf109880be 100644 --- a/static/app/components/core/composer/styles.tsx +++ b/static/app/components/core/composer/styles.tsx @@ -4,8 +4,7 @@ import {ListBox} from '@sentry/scraps/compactSelect'; import {Input} from '@sentry/scraps/input'; import {Flex} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; - -import type {FormSize} from 'sentry/utils/theme'; +import type {FormSize} from '@sentry/scraps/theme'; // Emotion retains Input's ref type after changing the rendered element. const InputDiv = Input.withComponent('div') as React.ComponentType< diff --git a/static/app/components/core/composer/types.ts b/static/app/components/core/composer/types.ts index 078c042627c2..0c2bd3d1720f 100644 --- a/static/app/components/core/composer/types.ts +++ b/static/app/components/core/composer/types.ts @@ -1,7 +1,7 @@ import type React from 'react'; import type {AnyUseQueryOptions} from '@tanstack/react-query'; -import type {FormSize} from 'sentry/utils/theme'; +import type {FormSize} from '@sentry/scraps/theme'; import type {ComposerValue} from './model'; diff --git a/static/app/components/core/descriptionList/descriptionList.tsx b/static/app/components/core/descriptionList/descriptionList.tsx index 37672f8f5a58..9450b3ae7bbd 100644 --- a/static/app/components/core/descriptionList/descriptionList.tsx +++ b/static/app/components/core/descriptionList/descriptionList.tsx @@ -4,8 +4,7 @@ import styled from '@emotion/styled'; import type {CSS} from '@sentry/scraps/cssTypes'; import {getSpacing, rc, type Responsive} from '@sentry/scraps/layout'; - -import type {SpaceSize} from 'sentry/utils/theme'; +import type {SpaceSize} from '@sentry/scraps/theme'; export interface DescriptionListProps extends React.HTMLAttributes { /** diff --git a/static/app/components/core/image/image.tsx b/static/app/components/core/image/image.tsx index 4cdb6d3ba7e0..d1bd2071ab19 100644 --- a/static/app/components/core/image/image.tsx +++ b/static/app/components/core/image/image.tsx @@ -3,8 +3,7 @@ import styled from '@emotion/styled'; import type {CSS} from '@sentry/scraps/cssTypes'; import type {Responsive} from '@sentry/scraps/layout'; import {getRadius, rc} from '@sentry/scraps/layout'; - -import type {RadiusSize} from 'sentry/utils/theme'; +import type {RadiusSize} from '@sentry/scraps/theme'; export interface ImageProps extends Omit< React.ImgHTMLAttributes, diff --git a/static/app/components/core/input/inputGroup.tsx b/static/app/components/core/input/inputGroup.tsx index 686bc3184660..3df49faab5e1 100644 --- a/static/app/components/core/input/inputGroup.tsx +++ b/static/app/components/core/input/inputGroup.tsx @@ -12,8 +12,7 @@ import {css} from '@emotion/react'; import styled from '@emotion/styled'; import type {InputProps} from '@sentry/scraps/input'; - -import type {FormSize, StrictCSSObject, Theme} from 'sentry/utils/theme'; +import type {FormSize, StrictCSSObject, Theme} from '@sentry/scraps/theme'; // There is a cycle here if we import textarea from scraps. // eslint-disable-next-line @sentry/no-relative-import-paths diff --git a/static/app/components/core/input/inputStyles.tsx b/static/app/components/core/input/inputStyles.tsx index 4de8c91caf63..4c63e1f1d19a 100644 --- a/static/app/components/core/input/inputStyles.tsx +++ b/static/app/components/core/input/inputStyles.tsx @@ -1,6 +1,6 @@ import {type Theme} from '@emotion/react'; -import {type FormSize, type StrictCSSObject} from 'sentry/utils/theme'; +import {type FormSize, type StrictCSSObject} from '@sentry/scraps/theme'; export interface InputStylesProps { monospace?: boolean; diff --git a/static/app/components/core/menuListItem/menuListItem.tsx b/static/app/components/core/menuListItem/menuListItem.tsx index d66e17b58675..5b9080f532fb 100644 --- a/static/app/components/core/menuListItem/menuListItem.tsx +++ b/static/app/components/core/menuListItem/menuListItem.tsx @@ -8,11 +8,11 @@ import {mergeRefs} from '@react-aria/utils'; import InteractionStateLayer from '@sentry/scraps/interactionStateLayer'; import {Container} from '@sentry/scraps/layout'; +import type {FormSize, Theme} from '@sentry/scraps/theme'; import type {TooltipProps} from '@sentry/scraps/tooltip'; import {Tooltip} from '@sentry/scraps/tooltip'; import {Overlay, PositionWrapper} from 'sentry/components/overlay'; -import type {FormSize, Theme} from 'sentry/utils/theme'; /** * Menu item priority. Determines the text and background color. diff --git a/static/app/components/core/principles/tokens/tokens.mdx b/static/app/components/core/principles/tokens/tokens.mdx index 49a3a8c09200..190ac6d4ce0a 100644 --- a/static/app/components/core/principles/tokens/tokens.mdx +++ b/static/app/components/core/principles/tokens/tokens.mdx @@ -80,7 +80,7 @@ For consistency, token scales and `variant` properties have been standardized wi > [!TIP] > -> `sentry/utils/theme` exposes standardized, public types for `SurfaceVariant`, `SemanticVariant`, `ContentVariant`, `GraphicsVariant`, and `BorderVariant`. +> `@sentry/scraps/theme` exposes standardized, public types for `SurfaceVariant`, `SemanticVariant`, `ContentVariant`, `GraphicsVariant`, and `BorderVariant`. | Keyword | Description | | ----------- | ------------------------------------------------------------- | diff --git a/static/app/components/core/radio/radio.tsx b/static/app/components/core/radio/radio.tsx index 45f86eff7a1d..0f98c6ca1bbc 100644 --- a/static/app/components/core/radio/radio.tsx +++ b/static/app/components/core/radio/radio.tsx @@ -1,8 +1,9 @@ import isPropValid from '@emotion/is-prop-valid'; import styled from '@emotion/styled'; +import type {StrictCSSObject, Theme} from '@sentry/scraps/theme'; + import {growIn} from 'sentry/styles/animations'; -import type {StrictCSSObject, Theme} from 'sentry/utils/theme'; interface RadioProps extends Omit< React.InputHTMLAttributes, diff --git a/static/app/components/core/renderToString.spec.tsx b/static/app/components/core/renderToString.spec.tsx deleted file mode 100644 index 82251db584d2..000000000000 --- a/static/app/components/core/renderToString.spec.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import {act, renderHook, renderHookWithProviders} from 'sentry-test/reactTestingLibrary'; - -import {Tag} from '@sentry/scraps/badge'; - -import {useRenderToString} from './renderToString'; - -describe('renderToString', () => { - it('should render a simple component to string', async () => { - function SimpleComponent() { - return
Hello, World!
; - } - - const {result} = renderHook(useRenderToString); - - const string = await act(() => result.current()); - - expect(string).toMatchInlineSnapshot('"
Hello, World!
"'); - }); - it('should render a scraps component to string', async () => { - function ScrapsComponent() { - return SuccessTag; - } - - const {result} = renderHookWithProviders(useRenderToString); - - const string = await act(() => result.current()); - - expect(string).toContain('data-test-id="tag-background"'); - expect(string).toContain('SuccessTag'); - }); -}); diff --git a/static/app/components/core/scraps.spec.tsx b/static/app/components/core/scraps.spec.tsx index 1a6e1bdb1269..b8edf5070a11 100644 --- a/static/app/components/core/scraps.spec.tsx +++ b/static/app/components/core/scraps.spec.tsx @@ -1,8 +1,10 @@ import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {Button, ButtonBar} from '@sentry/scraps/button'; import {InlineCode} from '@sentry/scraps/code'; import {Kbd} from '@sentry/scraps/hotkey'; import {Stack} from '@sentry/scraps/layout'; +import {Switch} from '@sentry/scraps/switch'; import {Prose, Text} from '@sentry/scraps/text'; it('composes package components through the remaining core barrels', () => { @@ -20,3 +22,15 @@ it('composes package components through the remaining core barrels', () => { expect(screen.getByText('Package code').tagName).toBe('CODE'); expect(screen.getByText('Package key').tagName).toBe('KBD'); }); + +it('composes moved controls with the remaining app button barrel', () => { + render( + + + + + ); + + expect(screen.getByRole('button', {name: 'Save'})).toBeEnabled(); + expect(screen.getByRole('checkbox', {name: 'Enabled'})).not.toBeChecked(); +}); diff --git a/static/app/components/core/segmentedControl/segmentedControl.tsx b/static/app/components/core/segmentedControl/segmentedControl.tsx index 19f6a533cbcc..341319035475 100644 --- a/static/app/components/core/segmentedControl/segmentedControl.tsx +++ b/static/app/components/core/segmentedControl/segmentedControl.tsx @@ -11,11 +11,10 @@ import type {CollectionChildren, Node} from '@react-types/shared'; import {LayoutGroup} from 'framer-motion'; import {DO_NOT_USE_getButtonStyles} from '@sentry/scraps/button'; +import type {FormSize, Theme} from '@sentry/scraps/theme'; import type {TooltipProps} from '@sentry/scraps/tooltip'; import {Tooltip} from '@sentry/scraps/tooltip'; -import type {FormSize, Theme} from 'sentry/utils/theme'; - type Priority = 'default' | 'primary' | 'secondary'; const getChildTransforms = (count: number) => { diff --git a/static/app/components/core/select/select.tsx b/static/app/components/core/select/select.tsx index ae34e197b7b4..e192a5829960 100644 --- a/static/app/components/core/select/select.tsx +++ b/static/app/components/core/select/select.tsx @@ -11,6 +11,7 @@ import {IconChevron} from '@sentry/icons/chevron'; import {IconClose} from '@sentry/icons/close'; import {Button} from '@sentry/scraps/button'; +import type {FormSize, Theme} from '@sentry/scraps/theme'; import {useTranslation} from '@sentry/scraps/translation/useTranslation'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; @@ -18,7 +19,6 @@ import type {Choices} from 'sentry/types/core'; import {convertFromSelect2Choices} from 'sentry/utils/convertFromSelect2Choices'; import {defined} from 'sentry/utils/defined'; import {PanelProvider} from 'sentry/utils/panelProvider'; -import type {FormSize, Theme} from 'sentry/utils/theme'; import {SelectOption} from './option'; import type { diff --git a/static/app/components/core/select/styles.tsx b/static/app/components/core/select/styles.tsx index b9235c1827c0..cf4ba745b83b 100644 --- a/static/app/components/core/select/styles.tsx +++ b/static/app/components/core/select/styles.tsx @@ -1,7 +1,7 @@ import {css} from '@emotion/react'; import styled from '@emotion/styled'; -import type {FormSize} from 'sentry/utils/theme'; +import type {FormSize} from '@sentry/scraps/theme'; export const CheckWrap = styled('div')<{ isMultiple: boolean; diff --git a/static/app/components/core/switch/switch.spec.tsx b/static/app/components/core/switch/switch.spec.tsx deleted file mode 100644 index 5349a756a5af..000000000000 --- a/static/app/components/core/switch/switch.spec.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; - -import {Switch} from '@sentry/scraps/switch'; - -describe('Switch', () => { - it('disabled', () => { - render(); - expect(screen.getByRole('checkbox')).toBeDisabled(); - }); - - it('checked', () => { - render(); - expect(screen.getByRole('checkbox')).toBeChecked(); - }); - - it('disallows toggling a disabled switch', async () => { - const onClick = jest.fn(); - - render(); - - const switchButton = screen.getByRole('checkbox'); - expect(switchButton).not.toBeChecked(); - - await userEvent.click(switchButton); - expect(switchButton).not.toBeChecked(); - expect(onClick).not.toHaveBeenCalled(); - }); -}); diff --git a/static/app/components/core/tabs/tab.tsx b/static/app/components/core/tabs/tab.tsx index fe7b3c4da36d..f95b1e464812 100644 --- a/static/app/components/core/tabs/tab.tsx +++ b/static/app/components/core/tabs/tab.tsx @@ -7,10 +7,9 @@ import type {TabListState} from '@react-stately/tabs'; import type {DOMAttributes, Node, Orientation} from '@react-types/shared'; import {Link} from '@sentry/scraps/link'; +import type {Theme} from '@sentry/scraps/theme'; import {Tooltip, type TooltipProps} from '@sentry/scraps/tooltip'; -import type {Theme} from 'sentry/utils/theme'; - import {tabsShouldForwardProp} from './utils'; interface BaseTabProps { diff --git a/static/app/components/core/toast/toast.tsx b/static/app/components/core/toast/toast.tsx index d19de2d5ce62..91c731c40bb0 100644 --- a/static/app/components/core/toast/toast.tsx +++ b/static/app/components/core/toast/toast.tsx @@ -6,10 +6,10 @@ import {IconWarning} from '@sentry/icons/warning'; import {Button} from '@sentry/scraps/button'; import {Container, Flex} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; +import type {Theme} from '@sentry/scraps/theme'; import {useTranslation} from '@sentry/scraps/translation/useTranslation'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; -import type {Theme} from 'sentry/utils/theme'; import {unreachable} from 'sentry/utils/unreachable'; import type {ToastAction, ToastVariant} from './types'; diff --git a/static/app/components/dnd/dragReorderButton.tsx b/static/app/components/dnd/dragReorderButton.tsx index 2aba6590dfca..d031fe5e0666 100644 --- a/static/app/components/dnd/dragReorderButton.tsx +++ b/static/app/components/dnd/dragReorderButton.tsx @@ -1,9 +1,9 @@ import {IconGrabbable} from '@sentry/icons/grabbable'; import {Button, type ButtonProps} from '@sentry/scraps/button'; +import type {IconSize} from '@sentry/scraps/theme'; import {t} from 'sentry/locale'; -import type {IconSize} from 'sentry/utils/theme'; type DragReorderButtonProps = Omit & { iconSize?: IconSize; diff --git a/static/app/components/droppedData/band/severityColor.spec.tsx b/static/app/components/droppedData/band/severityColor.spec.tsx index 2c10f8482bd5..ba858c82b2da 100644 --- a/static/app/components/droppedData/band/severityColor.spec.tsx +++ b/static/app/components/droppedData/band/severityColor.spec.tsx @@ -1,4 +1,4 @@ -import {darkTheme, lightTheme} from 'sentry/utils/theme/theme'; +import {darkTheme, lightTheme} from '@sentry/scraps/theme'; import {severityColor, withAlpha} from './severityColor'; diff --git a/static/app/components/group/externalIssuesList/issueTrackerActions.tsx b/static/app/components/group/externalIssuesList/issueTrackerActions.tsx index d276851bd355..7973645891fe 100644 --- a/static/app/components/group/externalIssuesList/issueTrackerActions.tsx +++ b/static/app/components/group/externalIssuesList/issueTrackerActions.tsx @@ -5,6 +5,7 @@ import {Button, type ButtonProps} from '@sentry/scraps/button'; import {DropdownMenu, type MenuItemProps} from '@sentry/scraps/dropdownMenu'; import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import {Text} from '@sentry/scraps/text'; +import type {Theme} from '@sentry/scraps/theme'; import {ErrorBoundary} from 'sentry/components/errorBoundary'; import type { @@ -13,7 +14,6 @@ import type { } from 'sentry/components/group/externalIssuesList/hooks/types'; import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; -import type {Theme} from 'sentry/utils/theme'; import {useOrganization} from 'sentry/utils/useOrganization'; const ISSUE_TRACKER_MENU_MAX_HEIGHT = 300; diff --git a/static/app/components/idBadge/baseBadge.tsx b/static/app/components/idBadge/baseBadge.tsx index b895f7b8d457..dee382b3ecf5 100644 --- a/static/app/components/idBadge/baseBadge.tsx +++ b/static/app/components/idBadge/baseBadge.tsx @@ -9,12 +9,12 @@ import { UserAvatar, } from '@sentry/scraps/avatar'; import {Flex, type FlexProps} from '@sentry/scraps/layout'; +import type {SpaceSize} from '@sentry/scraps/theme'; import type {Actor} from 'sentry/types/core'; import type {OrganizationSummary, Team} from 'sentry/types/organization'; import type {AvatarProject} from 'sentry/types/project'; import type {AvatarUser} from 'sentry/types/user'; -import type {SpaceSize} from 'sentry/utils/theme'; export interface BaseBadgeProps extends FlexProps { avatarProps?: Record; diff --git a/static/app/components/modals/dataWidgetViewerModal.tsx b/static/app/components/modals/dataWidgetViewerModal.tsx index e60cfa330379..4703476d2796 100644 --- a/static/app/components/modals/dataWidgetViewerModal.tsx +++ b/static/app/components/modals/dataWidgetViewerModal.tsx @@ -15,6 +15,7 @@ import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; import {Pagination} from '@sentry/scraps/pagination'; import {Select, SelectOption, components} from '@sentry/scraps/select'; import type {SelectValue} from '@sentry/scraps/select'; +import type {Theme} from '@sentry/scraps/theme'; import {Tooltip} from '@sentry/scraps/tooltip'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; @@ -48,7 +49,6 @@ import { decodeScalar, decodeSorts, } from 'sentry/utils/queryString'; -import type {Theme} from 'sentry/utils/theme'; import {useApi} from 'sentry/utils/useApi'; import {useLocation} from 'sentry/utils/useLocation'; import type {ReactRouter3Navigate} from 'sentry/utils/useNavigate'; diff --git a/static/app/components/modals/diffModal.tsx b/static/app/components/modals/diffModal.tsx index c22f5544011a..00fc22a19867 100644 --- a/static/app/components/modals/diffModal.tsx +++ b/static/app/components/modals/diffModal.tsx @@ -1,12 +1,13 @@ import {Fragment} from 'react'; import {css} from '@emotion/react'; +import type {Theme} from '@sentry/scraps/theme'; + import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import {IssueDiff} from 'sentry/components/issueDiff'; import {t} from 'sentry/locale'; import type {Project} from 'sentry/types/project'; import {useDetailedProject} from 'sentry/utils/project/useDetailedProject'; -import type {Theme} from 'sentry/utils/theme'; import {useOrganization} from 'sentry/utils/useOrganization'; interface Props extends ModalRenderProps, React.ComponentProps { diff --git a/static/app/components/onboarding/agenticProgress/agenticProgressList.tsx b/static/app/components/onboarding/agenticProgress/agenticProgressList.tsx index b03af850c1e1..7750d35bfda4 100644 --- a/static/app/components/onboarding/agenticProgress/agenticProgressList.tsx +++ b/static/app/components/onboarding/agenticProgress/agenticProgressList.tsx @@ -12,13 +12,13 @@ import {Tag} from '@sentry/scraps/badge'; import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; import {StatusIndicator} from '@sentry/scraps/statusIndicator'; import {Text} from '@sentry/scraps/text'; +import type {TagVariant} from '@sentry/scraps/theme'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {ProjectList} from 'sentry/components/projectList'; import {TimeSince} from 'sentry/components/timeSince'; import {t, tct, tn} from 'sentry/locale'; -import type {TagVariant} from 'sentry/utils/theme'; import {useProjects} from 'sentry/utils/useProjects'; import {FirstIssueCard} from './firstIssueCard'; diff --git a/static/app/components/onboarding/consts.ts b/static/app/components/onboarding/consts.ts index bb46e8bbf606..ae19c21c1af2 100644 --- a/static/app/components/onboarding/consts.ts +++ b/static/app/components/onboarding/consts.ts @@ -1,4 +1,4 @@ -import type {IconSize} from 'sentry/utils/theme'; +import type {IconSize} from '@sentry/scraps/theme'; /** * Shared layout constants for SCM onboarding steps. diff --git a/static/app/components/onboarding/scm/scmAlertFrequencySection.tsx b/static/app/components/onboarding/scm/scmAlertFrequencySection.tsx index 122408f82d06..c9e1005cbeff 100644 --- a/static/app/components/onboarding/scm/scmAlertFrequencySection.tsx +++ b/static/app/components/onboarding/scm/scmAlertFrequencySection.tsx @@ -3,9 +3,9 @@ import {IconInfo} from '@sentry/icons/info'; import {Tag} from '@sentry/scraps/badge'; import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; +import type {TagVariant} from '@sentry/scraps/theme'; import {t} from 'sentry/locale'; -import type {TagVariant} from 'sentry/utils/theme'; import {type IssueAlertNotificationProps} from 'sentry/views/projectInstall/issueAlertNotificationOptions'; import { type AlertRuleOptions, diff --git a/static/app/components/placeholder.tsx b/static/app/components/placeholder.tsx index be75b2e86858..9c8af3766118 100644 --- a/static/app/components/placeholder.tsx +++ b/static/app/components/placeholder.tsx @@ -1,7 +1,7 @@ import {css} from '@emotion/react'; import styled from '@emotion/styled'; -import type {SpaceSize} from 'sentry/utils/theme'; +import type {SpaceSize} from '@sentry/scraps/theme'; export interface PlaceholderProps { bottomGutter?: SpaceSize; diff --git a/static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx b/static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx index 0d9f60c26fd9..9da4bb46f957 100644 --- a/static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx +++ b/static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx @@ -2,6 +2,7 @@ import type {Theme} from '@emotion/react'; import styled from '@emotion/styled'; import {Container} from '@sentry/scraps/layout'; +import type {GraphicsVariant} from '@sentry/scraps/theme'; import {Tooltip} from '@sentry/scraps/tooltip'; import {BreadcrumbItem} from 'sentry/components/replays/breadcrumbs/breadcrumbItem'; @@ -13,7 +14,6 @@ import {useActiveReplayTab} from 'sentry/utils/replays/hooks/useActiveReplayTab' import {useCrumbHandlers} from 'sentry/utils/replays/hooks/useCrumbHandlers'; import type {ReplayFrame} from 'sentry/utils/replays/types'; import {isWebVitalFrame} from 'sentry/utils/replays/types'; -import type {GraphicsVariant} from 'sentry/utils/theme'; import {HoverOverlayGroupProvider} from 'sentry/utils/useHoverOverlay'; const NODE_SIZES = [8, 12, 16]; diff --git a/static/app/components/scrollCarousel.tsx b/static/app/components/scrollCarousel.tsx index e76deafc2451..9723e14d3b3b 100644 --- a/static/app/components/scrollCarousel.tsx +++ b/static/app/components/scrollCarousel.tsx @@ -6,10 +6,10 @@ import {IconChevron} from '@sentry/icons/chevron'; import color from 'color'; import {Button} from '@sentry/scraps/button'; +import type {SpaceSize} from '@sentry/scraps/theme'; import {t} from 'sentry/locale'; import {getOffsetRect} from 'sentry/utils/getOffsetRect'; -import type {SpaceSize} from 'sentry/utils/theme'; import {useRefChildrenVisibility} from 'sentry/utils/useRefChildrenVisibility'; interface ScrollCarouselProps { diff --git a/static/app/components/searchQueryBuilder/tokens/filter/regexDelimiter.tsx b/static/app/components/searchQueryBuilder/tokens/filter/regexDelimiter.tsx index eae3bdbfa832..981f8b7d2976 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/regexDelimiter.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/regexDelimiter.tsx @@ -1,7 +1,6 @@ import {Container} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; - -import type {SpaceSize} from 'sentry/utils/theme'; +import type {SpaceSize} from '@sentry/scraps/theme'; interface RegexDelimiterProps { onMouseDown?: React.MouseEventHandler; diff --git a/static/app/components/statusIndicator.tsx b/static/app/components/statusIndicator.tsx index e6b787c4c16e..2acb315afe1a 100644 --- a/static/app/components/statusIndicator.tsx +++ b/static/app/components/statusIndicator.tsx @@ -1,9 +1,8 @@ import styled from '@emotion/styled'; +import type {AlertVariant} from '@sentry/scraps/theme'; import {Tooltip} from '@sentry/scraps/tooltip'; -import type {AlertVariant} from 'sentry/utils/theme'; - type StatusVariant = AlertVariant | 'resolved'; export interface StatusIndicatorProps { status: StatusVariant; diff --git a/static/app/components/themeAndStyleProvider.tsx b/static/app/components/themeAndStyleProvider.tsx index 5b56acea413a..d1a3b295c4df 100644 --- a/static/app/components/themeAndStyleProvider.tsx +++ b/static/app/components/themeAndStyleProvider.tsx @@ -3,12 +3,13 @@ import {createPortal} from 'react-dom'; import createCache from '@emotion/cache'; import {CacheProvider, ThemeProvider} from '@emotion/react'; +import {darkTheme, lightTheme} from '@sentry/scraps/theme'; + import {printConsoleBanner} from 'sentry/bootstrap/printConsoleBanner'; import {NODE_ENV} from 'sentry/constants'; import {ConfigStore} from 'sentry/stores/configStore'; import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import {GlobalStyles} from 'sentry/styles/global'; -import {darkTheme, lightTheme} from 'sentry/utils/theme/theme'; const SentryComponentInspector = NODE_ENV === 'development' diff --git a/static/app/components/tours/components.tsx b/static/app/components/tours/components.tsx index ff3c45be2ff2..de699f5b5906 100644 --- a/static/app/components/tours/components.tsx +++ b/static/app/components/tours/components.tsx @@ -8,6 +8,7 @@ import {IconClose} from '@sentry/icons/close'; import {Button} from '@sentry/scraps/button'; import {useHotkeys} from '@sentry/scraps/hotkey'; import {Flex, Grid} from '@sentry/scraps/layout'; +import {useInvertedTheme} from '@sentry/scraps/theme'; import {Overlay, PositionWrapper} from 'sentry/components/overlay'; import { @@ -21,7 +22,6 @@ import {useMutateAssistant} from 'sentry/components/tours/useAssistant'; import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; import {defined} from 'sentry/utils/defined'; -import {useInvertedTheme} from 'sentry/utils/theme/useInvertedTheme'; import {useEffectAfterFirstRender} from 'sentry/utils/useEffectAfterFirstRender'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useOverlay, type UseOverlayProps} from 'sentry/utils/useOverlay'; diff --git a/static/app/components/tours/startTour.tsx b/static/app/components/tours/startTour.tsx index 7970b1ccc7c3..f0ebc3a6b7ac 100644 --- a/static/app/components/tours/startTour.tsx +++ b/static/app/components/tours/startTour.tsx @@ -2,10 +2,10 @@ import {css, ThemeProvider} from '@emotion/react'; import styled from '@emotion/styled'; import {Flex, Container} from '@sentry/scraps/layout'; +import {useInvertedTheme} from '@sentry/scraps/theme'; import {TextTourAction, TourAction} from 'sentry/components/tours/components'; import {t} from 'sentry/locale'; -import {useInvertedTheme} from 'sentry/utils/theme/useInvertedTheme'; interface StartTourModalProps { closeModal: () => void; diff --git a/static/app/components/workflowEngine/ui/alertsMonitorsShowcase.tsx b/static/app/components/workflowEngine/ui/alertsMonitorsShowcase.tsx index 3e5902f12abf..0169a2bf8cef 100644 --- a/static/app/components/workflowEngine/ui/alertsMonitorsShowcase.tsx +++ b/static/app/components/workflowEngine/ui/alertsMonitorsShowcase.tsx @@ -8,13 +8,13 @@ import monitorsTourMonitorsImage from 'sentry-images/spot/monitors-tour-monitors import {Stack} from '@sentry/scraps/layout'; import {ExternalLink} from '@sentry/scraps/link'; import {Text} from '@sentry/scraps/text'; +import type {Theme} from '@sentry/scraps/theme'; import {openModal, type ModalRenderProps} from 'sentry/actionCreators/modal'; import {FeatureShowcase} from 'sentry/components/featureShowcase'; import {t, tct} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; -import type {Theme} from 'sentry/utils/theme'; import {useOrganization} from 'sentry/utils/useOrganization'; const DOCS_URL = 'https://docs.sentry.io/product/new-monitors-and-alerts/'; diff --git a/static/app/debug/notifications/components/debugNotificationsLanding.tsx b/static/app/debug/notifications/components/debugNotificationsLanding.tsx index c02abdb135b3..d2a06b64a6ad 100644 --- a/static/app/debug/notifications/components/debugNotificationsLanding.tsx +++ b/static/app/debug/notifications/components/debugNotificationsLanding.tsx @@ -7,9 +7,8 @@ import heroImg from 'sentry-images/debug/notifications/hero.png'; import {Stack} from '@sentry/scraps/layout'; import {Heading, Text} from '@sentry/scraps/text'; - // Mimicking useStoriesDarkMode -> Don't use these elsewhere please 🙏 -import {darkTheme} from 'sentry/utils/theme/theme'; +import {darkTheme} from '@sentry/scraps/theme'; function DarkModeProvider(props: PropsWithChildren) { return {props.children}; diff --git a/static/app/stories/demo.tsx b/static/app/stories/demo.tsx index 66e3e76e6c47..8435fd2b32cf 100644 --- a/static/app/stories/demo.tsx +++ b/static/app/stories/demo.tsx @@ -10,9 +10,9 @@ import {Button} from '@sentry/scraps/button'; import {Container, Flex, Grid, type FlexProps} from '@sentry/scraps/layout'; import {RevealOnHover} from '@sentry/scraps/revealOnHover'; import {Text} from '@sentry/scraps/text'; +import type {ContainerBreakpointSize} from '@sentry/scraps/theme'; import {t} from 'sentry/locale'; -import type {ContainerBreakpointSize} from 'sentry/utils/theme'; import {useDimensions} from 'sentry/utils/useDimensions'; import {useFullscreen} from 'sentry/utils/window/useFullscreen'; import {useIsFullscreen} from 'sentry/utils/window/useIsFullscreen'; diff --git a/static/app/stories/view/useStoriesDarkMode.tsx b/static/app/stories/view/useStoriesDarkMode.tsx index d69b96b9bc9e..69aae31ddb3c 100644 --- a/static/app/stories/view/useStoriesDarkMode.tsx +++ b/static/app/stories/view/useStoriesDarkMode.tsx @@ -3,7 +3,7 @@ import {ThemeProvider, type Theme} from '@emotion/react'; // these utils are for stories that have forced dark mode // which is a very specific sanctioned use case -import {darkTheme} from 'sentry/utils/theme/theme'; +import {darkTheme} from '@sentry/scraps/theme'; /** * Access the raw values from the dark theme diff --git a/static/app/styles/global.tsx b/static/app/styles/global.tsx index 35ada7261dd7..a575e3547fe6 100644 --- a/static/app/styles/global.tsx +++ b/static/app/styles/global.tsx @@ -1,7 +1,7 @@ import type {Theme} from '@emotion/react'; import {css, Global} from '@emotion/react'; -import {useInvertedTheme} from 'sentry/utils/theme/useInvertedTheme'; +import {useInvertedTheme} from '@sentry/scraps/theme'; const generateThemePrismVariables = (theme: Theme, blockBackground: string) => ({ '--prism-base': theme.tokens.syntax.base, diff --git a/static/app/utils/performance/contexts/pageAlert.tsx b/static/app/utils/performance/contexts/pageAlert.tsx index 0dd550780d0d..ce709ea2222a 100644 --- a/static/app/utils/performance/contexts/pageAlert.tsx +++ b/static/app/utils/performance/contexts/pageAlert.tsx @@ -3,8 +3,8 @@ import {createContext, Fragment, useCallback, useContext, useState} from 'react' import {IconClose} from '@sentry/icons/close'; import {Alert} from '@sentry/scraps/alert'; +import type {AlertVariant} from '@sentry/scraps/theme'; -import type {AlertVariant} from 'sentry/utils/theme'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; enum DismissId { diff --git a/static/app/utils/replays/getFrameDetails.tsx b/static/app/utils/replays/getFrameDetails.tsx index 11317e34c510..d42930ccb104 100644 --- a/static/app/utils/replays/getFrameDetails.tsx +++ b/static/app/utils/replays/getFrameDetails.tsx @@ -22,6 +22,7 @@ import {IconWifi} from '@sentry/icons/wifi'; import {InfoTip} from '@sentry/scraps/info'; import {Flex} from '@sentry/scraps/layout'; import {ExternalLink} from '@sentry/scraps/link'; +import type {GraphicsVariant} from '@sentry/scraps/theme'; import {CrumbErrorTitle} from 'sentry/components/replays/breadcrumbs/errorTitle'; import {SelectorList} from 'sentry/components/replays/breadcrumbs/selectorList'; @@ -60,7 +61,6 @@ import { isRageClick, } from 'sentry/utils/replays/types'; import {toTitleCase} from 'sentry/utils/string/toTitleCase'; -import type {GraphicsVariant} from 'sentry/utils/theme'; import {stripURLOrigin} from 'sentry/utils/url/stripURLOrigin'; import {MODULE_DOC_LINK} from 'sentry/views/insights/browser/webVitals/settings'; diff --git a/static/app/utils/theme/index.tsx b/static/app/utils/theme/index.tsx deleted file mode 100644 index 605d0a0d6270..000000000000 --- a/static/app/utils/theme/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export type * from '@sentry/scraps/theme'; - -export type {StrictCSSObject, SentryTheme as Theme} from './theme'; diff --git a/static/app/utils/theme/theme.spec.tsx b/static/app/utils/theme/theme.spec.tsx deleted file mode 100644 index 3eb59f7da52c..000000000000 --- a/static/app/utils/theme/theme.spec.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import {useTheme} from '@emotion/react'; -import {expectTypeOf} from 'expect-type'; - -import {renderHookWithProviders} from 'sentry-test/reactTestingLibrary'; - -describe('theme', () => { - describe('getColorPalette', () => { - it('should return correct amount of colors', () => { - const {result} = renderHookWithProviders(useTheme); - const theme = result.current; - - expect(theme.chart.getColorPalette(2)).toHaveLength(3); - }); - - it('should return all colors when all is passed', () => { - const {result} = renderHookWithProviders(useTheme); - const theme = result.current; - - const colors = theme.chart.getColorPalette('all'); - - expect(colors).toHaveLength(18); - }); - - it('should have strict types', () => { - const {result} = renderHookWithProviders(useTheme); - const theme = result.current; - - const colors = theme.chart.getColorPalette(2); - - expectTypeOf(colors).toEqualTypeOf< - | readonly ['#7553FF', '#3A1873', '#F0369A'] - | readonly ['#7553FF', '#50219C', '#F0369A'] - >(); - }); - }); - - describe('swatch', () => { - it('should match old getColorPalette(9) colors for backward compatibility', () => { - const {result} = renderHookWithProviders(useTheme); - const theme = result.current; - - const oldPalette = theme.chart.getColorPalette(9); - const swatchColors = new Set(theme.swatch.values()); - - expect(swatchColors.size).toBe(10); - expect(swatchColors.size).toBe(oldPalette.length); - - for (const color of oldPalette) { - expect(swatchColors.has(color)).toBe(true); - } - }); - }); -}); diff --git a/static/app/utils/useBreakpoints.tsx b/static/app/utils/useBreakpoints.tsx index c849ce0bb2d4..25513d75bdf4 100644 --- a/static/app/utils/useBreakpoints.tsx +++ b/static/app/utils/useBreakpoints.tsx @@ -2,8 +2,9 @@ import {useEffect, useEffectEvent, useState} from 'react'; import {useTheme} from '@emotion/react'; import {Debouncer} from '@tanstack/react-pacer'; +import type {BreakpointSize} from '@sentry/scraps/theme'; + import {valueIsEqual} from 'sentry/utils/object/valueIsEqual'; -import type {BreakpointSize} from 'sentry/utils/theme'; export type Breakpoints = Record; diff --git a/static/app/utils/useHoverOverlay.tsx b/static/app/utils/useHoverOverlay.tsx index 785f6ba339d9..3aad53fd693c 100644 --- a/static/app/utils/useHoverOverlay.tsx +++ b/static/app/utils/useHoverOverlay.tsx @@ -17,9 +17,9 @@ import {useTheme} from '@emotion/react'; import {mergeProps} from '@react-aria/utils'; import type {CSS} from '@sentry/scraps/cssTypes'; +import type {Theme} from '@sentry/scraps/theme'; import {NODE_ENV} from 'sentry/constants/env'; -import type {Theme} from 'sentry/utils/theme'; import {useStableMergeRef} from './useStableMergeRef'; diff --git a/static/app/views/app/globalAlerts.tsx b/static/app/views/app/globalAlerts.tsx index dcf941f13d80..92c00aca0599 100644 --- a/static/app/views/app/globalAlerts.tsx +++ b/static/app/views/app/globalAlerts.tsx @@ -9,8 +9,9 @@ import { } from 'react'; import {uuid4} from '@sentry/core'; +import type {AlertVariant} from '@sentry/scraps/theme'; + import {localStorageWrapper} from 'sentry/utils/localStorage'; -import type {AlertVariant} from 'sentry/utils/theme'; export interface GlobalAlert { /** diff --git a/static/app/views/dashboards/revisionListItem.tsx b/static/app/views/dashboards/revisionListItem.tsx index 12b2134355bd..a963dce10829 100644 --- a/static/app/views/dashboards/revisionListItem.tsx +++ b/static/app/views/dashboards/revisionListItem.tsx @@ -6,12 +6,12 @@ import {UserAvatar} from '@sentry/scraps/avatar'; import {Tag} from '@sentry/scraps/badge'; import {Flex, Grid, Stack} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; +import type {TagVariant} from '@sentry/scraps/theme'; import {DateTime} from 'sentry/components/dateTime'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {t} from 'sentry/locale'; import type {User} from 'sentry/types/user'; -import type {TagVariant} from 'sentry/utils/theme'; import {useProjects} from 'sentry/utils/useProjects'; import type {DashboardRevision} from './hooks/useDashboardRevisions'; diff --git a/static/app/views/dashboards/widgets/heatMapWidget/plottables/heatMap.tsx b/static/app/views/dashboards/widgets/heatMapWidget/plottables/heatMap.tsx index eac3c3f3efd6..eb82a0614afa 100644 --- a/static/app/views/dashboards/widgets/heatMapWidget/plottables/heatMap.tsx +++ b/static/app/views/dashboards/widgets/heatMapWidget/plottables/heatMap.tsx @@ -1,6 +1,7 @@ import type {SeriesOption} from 'echarts'; -import type {Theme} from 'sentry/utils/theme'; +import type {Theme} from '@sentry/scraps/theme'; + import {ECHARTS_MISSING_DATA_VALUE} from 'sentry/utils/timeSeries/timeSeriesItemToEChartsDataPoint'; import {isAPlottableTimeSeriesValueType} from 'sentry/views/dashboards/widgets/common/typePredicates'; import type { diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/baselineMarkline.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/baselineMarkline.tsx index dc6efcb1384f..732191f437bc 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/baselineMarkline.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/baselineMarkline.tsx @@ -1,6 +1,7 @@ +import type {Theme} from '@sentry/scraps/theme'; + import {markLine} from 'sentry/components/charts/components/markLine'; import {t} from 'sentry/locale'; -import type {Theme} from 'sentry/utils/theme'; interface Props { theme: Theme; diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/thresholds.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/thresholds.tsx index 47315d54d552..051185c0e637 100644 --- a/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/thresholds.tsx +++ b/static/app/views/dashboards/widgets/timeSeriesWidget/plottables/thresholds.tsx @@ -4,10 +4,11 @@ import type { SeriesOption, } from 'echarts'; +import type {Theme} from '@sentry/scraps/theme'; + import {MarkArea} from 'sentry/components/charts/components/markArea'; import {markLine} from 'sentry/components/charts/components/markLine'; import {t} from 'sentry/locale'; -import type {Theme} from 'sentry/utils/theme'; import {normalizeUnit} from 'sentry/views/dashboards/utils/normalizeUnit'; import { NEGATIVE_POLARITY_COLOR_ORDER, diff --git a/static/app/views/explore/components/attributeBreakdowns/utils.tsx b/static/app/views/explore/components/attributeBreakdowns/utils.tsx index 997e944c9ff1..3fea8971ab3f 100644 --- a/static/app/views/explore/components/attributeBreakdowns/utils.tsx +++ b/static/app/views/explore/components/attributeBreakdowns/utils.tsx @@ -1,6 +1,7 @@ +import type {Theme} from '@sentry/scraps/theme'; + import {t} from 'sentry/locale'; import {escape} from 'sentry/utils'; -import type {Theme} from 'sentry/utils/theme'; import type {AttributeDistribution} from 'sentry/views/explore/components/attributeBreakdowns/attributeDistributionContent'; import {Actions} from 'sentry/views/explore/hooks/useAttributeBreakdownsTooltip'; diff --git a/static/app/views/insights/pages/agents/components/modelName.tsx b/static/app/views/insights/pages/agents/components/modelName.tsx index bd9ad5523bf6..6105b501136e 100644 --- a/static/app/views/insights/pages/agents/components/modelName.tsx +++ b/static/app/views/insights/pages/agents/components/modelName.tsx @@ -2,9 +2,9 @@ import styled from '@emotion/styled'; import {PlatformIcon} from 'platformicons'; import {Flex} from '@sentry/scraps/layout'; +import type {SpaceSize} from '@sentry/scraps/theme'; import {t} from 'sentry/locale'; -import type {SpaceSize} from 'sentry/utils/theme'; interface ModelNameProps { modelId: string | null; diff --git a/static/app/views/navigation/primary/navigationMenuHeader.tsx b/static/app/views/navigation/primary/navigationMenuHeader.tsx index efae2793fa8f..07a7f4f5b49f 100644 --- a/static/app/views/navigation/primary/navigationMenuHeader.tsx +++ b/static/app/views/navigation/primary/navigationMenuHeader.tsx @@ -1,6 +1,7 @@ import type {ReactNode} from 'react'; import {css, ThemeProvider, useTheme} from '@emotion/react'; +import {useInvertedTheme} from '@sentry/scraps'; import { getOrganizationAvatarProps, getUserAvatarProps, @@ -15,7 +16,6 @@ import {Text} from '@sentry/scraps/text'; import {t, tn} from 'sentry/locale'; import type {OrganizationSummary} from 'sentry/types/organization'; import type {User} from 'sentry/types/user'; -import {useInvertedTheme} from 'sentry/utils/theme/useInvertedTheme'; type NavigationMenuHeaderProps = | {organization: OrganizationSummary; projectCount: number; user?: never} diff --git a/static/app/views/organizationStats/usageChart/index.tsx b/static/app/views/organizationStats/usageChart/index.tsx index 8929c5e11a74..4510bbdce9ec 100644 --- a/static/app/views/organizationStats/usageChart/index.tsx +++ b/static/app/views/organizationStats/usageChart/index.tsx @@ -4,6 +4,7 @@ import {IconWarning} from '@sentry/icons/warning'; import type {BarSeriesOption, LegendComponentOption, SeriesOption} from 'echarts'; import type {SelectValue} from '@sentry/scraps/select'; +import type {Theme} from '@sentry/scraps/theme'; import type {BaseChartProps} from 'sentry/components/charts/baseChart'; import {BaseChart} from 'sentry/components/charts/baseChart'; @@ -19,7 +20,6 @@ import {t} from 'sentry/locale'; import type {DataCategory, IntervalPeriod} from 'sentry/types/core'; import {parsePeriodToHours} from 'sentry/utils/duration/parsePeriodToHours'; import {statsPeriodToDays} from 'sentry/utils/duration/statsPeriodToDays'; -import type {Theme} from 'sentry/utils/theme'; import {formatUsageWithUnits} from 'sentry/views/organizationStats/utils'; import {getTooltipFormatter, getXAxisDates, getXAxisLabelVisibility} from './utils'; diff --git a/static/app/views/performance/traceDetails/traceDrawer/details/durationComparison.tsx b/static/app/views/performance/traceDetails/traceDrawer/details/durationComparison.tsx index 84543d95ddb4..0ff3f6622059 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/details/durationComparison.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/details/durationComparison.tsx @@ -1,10 +1,10 @@ import type {Theme} from '@emotion/react'; +import type {TagVariant} from '@sentry/scraps/theme'; import {Tooltip} from '@sentry/scraps/tooltip'; import {tct} from 'sentry/locale'; import {getDuration} from 'sentry/utils/duration/getDuration'; -import type {TagVariant} from 'sentry/utils/theme'; /** Hide comparisons below this delta so tiny, noisy differences aren't shown. */ export const MIN_PCT_DURATION_DIFFERENCE = 10; diff --git a/static/app/views/performance/traceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx b/static/app/views/performance/traceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx index 60d75dbf794f..d4856c09a074 100644 --- a/static/app/views/performance/traceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx +++ b/static/app/views/performance/traceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx @@ -2,6 +2,8 @@ import {SEARCH_SENTRY__LINK__TYPE} from '@sentry/conventions/attributes/search'; import type {Location} from 'history'; import countBy from 'lodash/countBy'; +import type {Theme} from '@sentry/scraps/theme'; + import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import { @@ -9,7 +11,6 @@ import { type RenderFunctionBaggage, } from 'sentry/utils/discover/fieldRenderers'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; -import type {Theme} from 'sentry/utils/theme'; import {useNavigate} from 'sentry/utils/useNavigate'; import { AttributesTree, diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx index d9743b5c2c67..2531b7831f94 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx @@ -10,6 +10,7 @@ import {Container} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import {Pagination, type CursorHandler} from '@sentry/scraps/pagination'; import {COL_WIDTH_UNDEFINED} from '@sentry/scraps/table'; +import type {Theme} from '@sentry/scraps/theme'; import {Tooltip} from '@sentry/scraps/tooltip'; import {Duration} from 'sentry/components/duration'; @@ -33,7 +34,6 @@ import {toPercent} from 'sentry/utils/number/toPercent'; import {isEmptyObject} from 'sentry/utils/object/isEmptyObject'; import {decodeScalar, decodeSorts} from 'sentry/utils/queryString'; import {projectSupportsReplay} from 'sentry/utils/replays/projectSupportsReplay'; -import type {Theme} from 'sentry/utils/theme'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useLocation} from 'sentry/utils/useLocation'; diff --git a/static/app/views/preprod/snapshots/main/snapshotCards.tsx b/static/app/views/preprod/snapshots/main/snapshotCards.tsx index 3c361ab3ca0b..e6689900e44c 100644 --- a/static/app/views/preprod/snapshots/main/snapshotCards.tsx +++ b/static/app/views/preprod/snapshots/main/snapshotCards.tsx @@ -15,12 +15,12 @@ import {CodeBlock} from '@sentry/scraps/code'; import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; import type {ContentVariant} from '@sentry/scraps/theme'; +import {darkTheme, lightTheme} from '@sentry/scraps/theme'; import {Tooltip} from '@sentry/scraps/tooltip'; import {t} from 'sentry/locale'; import {ConfigStore} from 'sentry/stores/configStore'; import {formatPercentage} from 'sentry/utils/number/formatPercentage'; -import {darkTheme, lightTheme} from 'sentry/utils/theme/theme'; import {useCopyToClipboard} from 'sentry/utils/useCopyToClipboard'; import type { SnapshotDiffPair, diff --git a/static/app/views/seerWorkflows/agenticTriage.tsx b/static/app/views/seerWorkflows/agenticTriage.tsx index 72d5a6cb3bbd..8926e4f36534 100644 --- a/static/app/views/seerWorkflows/agenticTriage.tsx +++ b/static/app/views/seerWorkflows/agenticTriage.tsx @@ -9,13 +9,13 @@ import {LinkButton} from '@sentry/scraps/button'; import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import {Prose, Text} from '@sentry/scraps/text'; +import type {TagVariant} from '@sentry/scraps/theme'; import {Tooltip} from '@sentry/scraps/tooltip'; import {getPullRequestStatusLabel} from 'sentry/components/group/externalIssuesList/pullRequestStatusBadge'; import {t, tn} from 'sentry/locale'; import type {PullRequestStatus} from 'sentry/types/integrations'; import {MarkedText} from 'sentry/utils/marked/markedText'; -import type {TagVariant} from 'sentry/utils/theme'; import {getRelativeExplorerUrl} from 'sentry/views/seerExplorer/utils'; import type { SeerAgenticTriageRun, diff --git a/static/app/views/seerWorkflows/overview/changedFileRow.tsx b/static/app/views/seerWorkflows/overview/changedFileRow.tsx index 19446e87901c..06eff9a9d4b4 100644 --- a/static/app/views/seerWorkflows/overview/changedFileRow.tsx +++ b/static/app/views/seerWorkflows/overview/changedFileRow.tsx @@ -6,8 +6,8 @@ import {Tag} from '@sentry/scraps/badge'; import {Button} from '@sentry/scraps/button'; import {Container, Flex} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; +import type {TagVariant} from '@sentry/scraps/theme'; -import type {TagVariant} from 'sentry/utils/theme'; import {OpenFileButton} from 'sentry/views/seerExplorer/components/openFileButton'; export interface FileChangeTag { diff --git a/static/gsAdmin/views/layout.tsx b/static/gsAdmin/views/layout.tsx index 94faec30e7d0..eddd13f7c8fd 100644 --- a/static/gsAdmin/views/layout.tsx +++ b/static/gsAdmin/views/layout.tsx @@ -12,11 +12,11 @@ import {Button} from '@sentry/scraps/button'; import {Container, Flex, Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import {GlobalModal} from '@sentry/scraps/modal'; +import {darkTheme, lightTheme} from '@sentry/scraps/theme'; import {ListLink} from 'sentry/components/links/listLink'; import {ScrapsProviders} from 'sentry/scrapsProviders'; import {localStorageWrapper} from 'sentry/utils/localStorage'; -import {darkTheme, lightTheme} from 'sentry/utils/theme/theme'; import {GlobalAlertProvider} from 'sentry/views/app/globalAlerts'; import {SystemAlerts} from 'sentry/views/app/systemAlerts'; diff --git a/static/gsApp/components/gsBanner.tsx b/static/gsApp/components/gsBanner.tsx index 022854c5b4ec..53aaa71e8f92 100644 --- a/static/gsApp/components/gsBanner.tsx +++ b/static/gsApp/components/gsBanner.tsx @@ -9,6 +9,7 @@ import {Alert, type AlertProps} from '@sentry/scraps/alert'; import {Tag} from '@sentry/scraps/badge'; import {Button, LinkButton} from '@sentry/scraps/button'; import {Flex, Grid} from '@sentry/scraps/layout'; +import {useInvertedTheme} from '@sentry/scraps/theme'; import type {ModalRenderProps} from 'sentry/actionCreators/modal'; import {openModal} from 'sentry/actionCreators/modal'; @@ -23,7 +24,6 @@ import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {showIntercom} from 'sentry/utils/intercom'; import {promptIsDismissed} from 'sentry/utils/promptIsDismissed'; import {RequestError} from 'sentry/utils/requestError/requestError'; -import {useInvertedTheme} from 'sentry/utils/theme/useInvertedTheme'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useNavigate} from 'sentry/utils/useNavigate'; import {withApi} from 'sentry/utils/withApi'; diff --git a/static/gsApp/views/seerAutomation/components/seerConnectGitHubBanner.tsx b/static/gsApp/views/seerAutomation/components/seerConnectGitHubBanner.tsx index 530a02698daa..ef88b519b91e 100644 --- a/static/gsApp/views/seerAutomation/components/seerConnectGitHubBanner.tsx +++ b/static/gsApp/views/seerAutomation/components/seerConnectGitHubBanner.tsx @@ -7,10 +7,10 @@ import seerConfigMainBg from 'sentry-images/spot/seer-config-main-bg.svg'; import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout'; import {Heading, Text} from '@sentry/scraps/text'; +import {useInvertedTheme} from '@sentry/scraps/theme'; import {t} from 'sentry/locale'; import {getSeerOnboardingCheckQueryOptions} from 'sentry/utils/getSeerOnboardingCheckQueryOptions'; -import {useInvertedTheme} from 'sentry/utils/theme/useInvertedTheme'; import {useOrganization} from 'sentry/utils/useOrganization'; import {GithubButton} from 'getsentry/views/seerAutomation/onboarding/githubButton'; diff --git a/static/packages/scraps/README.md b/static/packages/scraps/README.md index 5e1d41ac48c2..d853770d17b8 100644 --- a/static/packages/scraps/README.md +++ b/static/packages/scraps/README.md @@ -55,6 +55,8 @@ pnpm --dir static/packages/scraps verify Verification deletes `dist` before packing to exercise `prepack`, checks the tarball contents and dependencies, typechecks and imports each public export, and server renders representative components in both themes. +The verifier compiles a temporary copy of the source-only icons workspace for +the test consumer. It does not change or publish the icons package. Consumers provide Emotion's `ThemeProvider` with `lightTheme` or `darkTheme`. The package does not include application providers, global CSS, or fonts. diff --git a/static/packages/scraps/package.json b/static/packages/scraps/package.json index e724963c308b..01db5d55a678 100644 --- a/static/packages/scraps/package.json +++ b/static/packages/scraps/package.json @@ -42,6 +42,54 @@ "./loader": { "types": "./dist/loader/index.d.ts", "import": "./dist/loader/index.js" + }, + "./backdrop": { + "types": "./dist/backdrop/index.d.ts", + "import": "./dist/backdrop/index.js" + }, + "./emptyState": { + "types": "./dist/emptyState/index.d.ts", + "import": "./dist/emptyState/index.js" + }, + "./revealOnHover": { + "types": "./dist/revealOnHover/index.d.ts", + "import": "./dist/revealOnHover/index.js" + }, + "./slider": { + "types": "./dist/slider/index.d.ts", + "import": "./dist/slider/index.js" + }, + "./slot": { + "types": "./dist/slot/index.d.ts", + "import": "./dist/slot/index.js" + }, + "./switch": { + "types": "./dist/switch/index.d.ts", + "import": "./dist/switch/index.js" + }, + "./translation": { + "types": "./dist/translation/index.d.ts", + "import": "./dist/translation/index.js" + }, + "./boundaryContext": { + "types": "./dist/boundaryContext.d.ts", + "import": "./dist/boundaryContext.js" + }, + "./datetime": { + "types": "./dist/datetime.d.ts", + "import": "./dist/datetime.js" + }, + "./sizeContext": { + "types": "./dist/sizeContext.d.ts", + "import": "./dist/sizeContext.js" + }, + "./renderToString": { + "types": "./dist/renderToString.d.ts", + "import": "./dist/renderToString.js" + }, + "./useScrollLock": { + "types": "./dist/useScrollLock.d.ts", + "import": "./dist/useScrollLock.js" } }, "publishConfig": { @@ -57,8 +105,15 @@ "dependencies": { "@emotion/is-prop-valid": "^1.4.0", "@emotion/styled": "^11.14.0", + "@react-aria/i18n": "3.13.1", + "@react-aria/slider": "3.9.1", "@react-aria/utils": "3.34.1", - "@types/react": "19.3.0" + "@react-aria/visually-hidden": "3.9.1", + "@react-stately/slider": "3.8.1", + "@sentry/icons": "workspace:*", + "@sentry/react": "11.6.0", + "@types/react": "19.3.0", + "framer-motion": "12.38.0" }, "devDependencies": { "@jest/environment": "30.5.1", @@ -70,8 +125,10 @@ "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "7.0.1", "@testing-library/react": "16.3.2", + "@testing-library/user-event": "14.6.7", "@types/jest": "30.0.0", "@types/node": "26.6.4", + "@types/react-dom": "19.3.0", "expect-type": "1.4.0", "jest": "30.5.1", "jsdom": "30.1.1", diff --git a/static/packages/scraps/rslib.config.mjs b/static/packages/scraps/rslib.config.mjs index 0c7301115f47..f86f1770aeee 100644 --- a/static/packages/scraps/rslib.config.mjs +++ b/static/packages/scraps/rslib.config.mjs @@ -2,7 +2,7 @@ import {pluginReact} from '@rsbuild/plugin-react'; import {defineConfig} from '@rslib/core'; export default defineConfig({ - lib: [{format: 'esm', bundle: false, dts: true, syntax: 'es2022'}], + lib: [{format: 'esm', bundle: false, dts: {build: true}, syntax: 'es2022'}], source: { entry: {index: ['./src/**', '!src/**/*.spec.tsx']}, }, diff --git a/static/packages/scraps/scripts/verify.mjs b/static/packages/scraps/scripts/verify.mjs index ca25e192f4b7..42bf705e839d 100644 --- a/static/packages/scraps/scripts/verify.mjs +++ b/static/packages/scraps/scripts/verify.mjs @@ -3,6 +3,9 @@ import {mkdtemp, mkdir, readFile, readdir, rm, writeFile} from 'node:fs/promises import {join, resolve} from 'node:path'; import {fileURLToPath} from 'node:url'; +import {pluginReact} from '@rsbuild/plugin-react'; +import {createRslib} from '@rslib/core'; + const root = fileURLToPath(new URL('../', import.meta.url)); const packageJson = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')); const temporary = await mkdtemp(join(root, '.verify-')); @@ -13,6 +16,10 @@ const tarball = join(temporary, 'scraps.tgz'); try { await rm(join(root, 'dist'), {recursive: true, force: true}); await mkdir(installed, {recursive: true}); + await writeFile( + join(consumer, 'package.json'), + JSON.stringify({name: 'scraps-verify-consumer', private: true, type: 'module'}) + ); execFileSync('pnpm', ['pack', '--out', tarball], {cwd: root, stdio: 'inherit'}); execFileSync('tar', ['-xzf', tarball, '-C', installed, '--strip-components=1']); @@ -42,6 +49,40 @@ try { } } + // Icons currently exports workspace source. Compile a temporary dependency so + // Node can verify the packed Scraps JavaScript without a TSX loader. + const iconsRoot = fileURLToPath(new URL('../../icons/', import.meta.url)); + const installedIcons = join(consumer, 'node_modules', '@sentry', 'icons'); + const iconsPackage = JSON.parse( + await readFile(join(iconsRoot, 'package.json'), 'utf8') + ); + const iconsBuild = await createRslib({ + cwd: iconsRoot, + config: { + lib: [{format: 'esm', bundle: false, syntax: 'es2022'}], + source: {entry: {index: ['./src/**', '!src/**/*.spec.tsx']}}, + output: { + target: 'node', + distPath: {root: join(installedIcons, 'dist')}, + cleanDistPath: false, + }, + plugins: [pluginReact()], + }, + }); + await iconsBuild.build(); + await writeFile( + join(installedIcons, 'package.json'), + JSON.stringify({ + ...iconsPackage, + exports: Object.fromEntries( + Object.entries(iconsPackage.exports).map(([subpath, source]) => [ + subpath, + source.replace('./src/', './dist/').replace(/\.tsx?$/, '.js'), + ]) + ), + }) + ); + const config = { compilerOptions: { target: 'ES2022', @@ -100,6 +141,7 @@ for (const subpath of ['layout/stack', 'theme/light', 'tokens/color', 'cssTypes' InlineCode, Kbd, IndeterminateLoader, + Switch, lightTheme, darkTheme, } = await import(join(installed, 'dist/index.js')); @@ -125,7 +167,8 @@ for (const subpath of ['layout/stack', 'theme/light', 'tokens/color', 'cssTypes' createElement(Quote, null, 'Quote'), createElement(InlineCode, null, 'InlineCode'), createElement(Kbd, null, 'Kbd'), - createElement(IndeterminateLoader) + createElement(IndeterminateLoader), + createElement(Switch, {'aria-label': 'Switch'}) ) ) ); @@ -139,6 +182,8 @@ for (const subpath of ['layout/stack', 'theme/light', 'tokens/color', 'cssTypes' 'Quote', 'InlineCode', 'Kbd', + 'data-icon="checkmark"', + 'data-icon="close"', ]) { if (!html.includes(label)) { throw new Error(`Server render failed for ${label}`); diff --git a/static/app/components/core/backdrop/backdrop.tsx b/static/packages/scraps/src/backdrop/backdrop.tsx similarity index 100% rename from static/app/components/core/backdrop/backdrop.tsx rename to static/packages/scraps/src/backdrop/backdrop.tsx diff --git a/static/app/components/core/backdrop/index.tsx b/static/packages/scraps/src/backdrop/index.tsx similarity index 100% rename from static/app/components/core/backdrop/index.tsx rename to static/packages/scraps/src/backdrop/index.tsx diff --git a/static/app/components/core/boundaryContext.tsx b/static/packages/scraps/src/boundaryContext.tsx similarity index 100% rename from static/app/components/core/boundaryContext.tsx rename to static/packages/scraps/src/boundaryContext.tsx diff --git a/static/app/components/core/datetime.spec.tsx b/static/packages/scraps/src/datetime.spec.tsx similarity index 87% rename from static/app/components/core/datetime.spec.tsx rename to static/packages/scraps/src/datetime.spec.tsx index 965d4c424dee..4c7c1b90c3ba 100644 --- a/static/app/components/core/datetime.spec.tsx +++ b/static/packages/scraps/src/datetime.spec.tsx @@ -1,6 +1,6 @@ -import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {render, screen} from '@testing-library/react'; -import {DateTimeProvider, useClockDisplay, useTimezone} from '@sentry/scraps/datetime'; +import {DateTimeProvider, useClockDisplay, useTimezone} from './datetime'; describe('datetime', () => { function ShowDateTime() { diff --git a/static/app/components/core/datetime.tsx b/static/packages/scraps/src/datetime.tsx similarity index 100% rename from static/app/components/core/datetime.tsx rename to static/packages/scraps/src/datetime.tsx diff --git a/static/app/components/core/emptyState/emptyState.spec.tsx b/static/packages/scraps/src/emptyState/emptyState.spec.tsx similarity index 66% rename from static/app/components/core/emptyState/emptyState.spec.tsx rename to static/packages/scraps/src/emptyState/emptyState.spec.tsx index c7da69d13b71..e09d3ff07c59 100644 --- a/static/app/components/core/emptyState/emptyState.spec.tsx +++ b/static/packages/scraps/src/emptyState/emptyState.spec.tsx @@ -1,18 +1,26 @@ -import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; -import {EmptyState} from '@sentry/scraps/emptyState'; +import {lightTheme} from '@sentry/scraps/theme'; + +import {EmptyState} from './index'; + +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} describe('EmptyState', () => { it('renders title and description', () => { render( - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('No results found')).toBeInTheDocument(); expect(screen.getByText('Try adjusting your search.')).toBeInTheDocument(); }); it('renders title only', () => { - render(); + render(, {wrapper: ThemeWrapper}); expect(screen.getByText('Nothing here')).toBeInTheDocument(); }); @@ -21,14 +29,19 @@ describe('EmptyState', () => { } - /> + />, + {wrapper: ThemeWrapper} ); expect(screen.getByRole('img')).toBeInTheDocument(); }); it('renders action', () => { render( - Add repository} /> + Add repository} + />, + {wrapper: ThemeWrapper} ); expect(screen.getByRole('button', {name: 'Add repository'})).toBeInTheDocument(); }); @@ -40,7 +53,8 @@ describe('EmptyState', () => { description="Autofix requires a connected repo." illustration={illo} action={} - /> + />, + {wrapper: ThemeWrapper} ); expect(screen.getByTestId('empty-state')).toBeInTheDocument(); expect(screen.getByText('Connect a repository')).toBeInTheDocument(); diff --git a/static/app/components/core/emptyState/emptyState.tsx b/static/packages/scraps/src/emptyState/emptyState.tsx similarity index 93% rename from static/app/components/core/emptyState/emptyState.tsx rename to static/packages/scraps/src/emptyState/emptyState.tsx index ffdefd8bce8f..4030c0c90149 100644 --- a/static/app/components/core/emptyState/emptyState.tsx +++ b/static/packages/scraps/src/emptyState/emptyState.tsx @@ -1,5 +1,6 @@ import {Flex, Stack, type FlexProps} from '@sentry/scraps/layout'; -import {Heading, Text, type TextProps} from '@sentry/scraps/text'; +import {Heading} from '@sentry/scraps/text/heading'; +import {Text, type TextProps} from '@sentry/scraps/text/text'; interface EmptyStateProps extends Omit< FlexProps, diff --git a/static/app/components/core/emptyState/index.tsx b/static/packages/scraps/src/emptyState/index.tsx similarity index 100% rename from static/app/components/core/emptyState/index.tsx rename to static/packages/scraps/src/emptyState/index.tsx diff --git a/static/packages/scraps/src/hotkey/kbd.spec.tsx b/static/packages/scraps/src/hotkey/kbd.spec.tsx index f81fdfed0e25..08e947908897 100644 --- a/static/packages/scraps/src/hotkey/kbd.spec.tsx +++ b/static/packages/scraps/src/hotkey/kbd.spec.tsx @@ -1,21 +1,28 @@ -import {render, screen} from '../../test/env'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; + +import {lightTheme} from '@sentry/scraps/theme'; import {Kbd} from './kbd'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + describe('Kbd', () => { it('renders a kbd element', () => { - render(K); + render(K, {wrapper: ThemeWrapper}); const el = screen.getByText('K'); expect(el.tagName).toBe('KBD'); }); it('forwards className', () => { - render(X); + render(X, {wrapper: ThemeWrapper}); expect(screen.getByText('X')).toHaveClass('custom'); }); it('renders glyph characters', () => { - render({'\u2318'}); + render({'\u2318'}, {wrapper: ThemeWrapper}); expect(screen.getByText('\u2318')).toBeInTheDocument(); }); }); diff --git a/static/packages/scraps/src/index.ts b/static/packages/scraps/src/index.ts index f9b7a23e0655..04d258e1bcf9 100644 --- a/static/packages/scraps/src/index.ts +++ b/static/packages/scraps/src/index.ts @@ -7,3 +7,15 @@ export {InlineCode} from './code/inlineCode'; export {Kbd} from './hotkey/kbd'; export * from './theme'; export * from './tokens'; +export * from './backdrop'; +export * from './emptyState'; +export * from './revealOnHover'; +export * from './slider'; +export * from './slot'; +export * from './switch'; +export * from './translation'; +export * from './boundaryContext'; +export * from './datetime'; +export * from './sizeContext'; +export * from './renderToString'; +export * from './useScrollLock'; diff --git a/static/packages/scraps/src/layout/container.spec.tsx b/static/packages/scraps/src/layout/container.spec.tsx index 396036d7e1ee..3ca752cfa6af 100644 --- a/static/packages/scraps/src/layout/container.spec.tsx +++ b/static/packages/scraps/src/layout/container.spec.tsx @@ -1,4 +1,6 @@ import {createRef, Fragment} from 'react'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; import {expectTypeOf} from 'expect-type'; import { @@ -6,12 +8,15 @@ import { type ContainerProps, type ContainerPropsWithRenderFunction, } from '@sentry/scraps/layout'; +import {lightTheme} from '@sentry/scraps/theme'; -import {render, screen} from '../../test/env'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} describe('Container', () => { it('renders children', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).toBeInTheDocument(); }); @@ -19,7 +24,8 @@ describe('Container', () => { render(
{props =>

Hello

}
-
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')?.tagName).toBe('P'); @@ -34,7 +40,8 @@ describe('Container', () => { {/* @ts-expect-error - this should be a React.ElementType */} {props =>

Hello

} -
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('aria-activedescendant'); @@ -50,7 +57,8 @@ describe('Container', () => { {/* @ts-expect-error - className is incompatible */} {props => } - + , + {wrapper: ThemeWrapper} ); }); @@ -58,28 +66,31 @@ describe('Container', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); expectTypeOf>().toHaveProperty('htmlFor'); }); it('passes attributes to the underlying element', () => { - render(Hello); + render(Hello, { + wrapper: ThemeWrapper, + }); expect(screen.getByTestId('container')).toBeInTheDocument(); }); it('renders as a different element if specified', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello').tagName).toBe('SECTION'); }); it('does not bleed attributes to the underlying element', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).not.toHaveAttribute('radius'); }); it('allows settings native html attributes', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).toHaveStyle({color: 'rgb(255, 0, 0)'}); }); @@ -88,7 +99,8 @@ describe('Container', () => { render( Hello - + , + {wrapper: ThemeWrapper} ); expect(ref.current).toBeInTheDocument(); expect(ref.current?.tagName).toBe('OL'); @@ -103,7 +115,8 @@ describe('Container', () => { PaddingBottom First - + , + {wrapper: ThemeWrapper} ); const paddingFirst = screen.getByText('Padding First').className; diff --git a/static/packages/scraps/src/layout/flex.spec.tsx b/static/packages/scraps/src/layout/flex.spec.tsx index 0bf3525dfc0b..79299c101b61 100644 --- a/static/packages/scraps/src/layout/flex.spec.tsx +++ b/static/packages/scraps/src/layout/flex.spec.tsx @@ -1,15 +1,20 @@ import {createRef, Fragment} from 'react'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; import {expectTypeOf} from 'expect-type'; import type {Responsive} from '@sentry/scraps/layout'; - -import {render, screen} from '../../test/env'; +import {lightTheme} from '@sentry/scraps/theme'; import {Flex, type FlexProps, type FlexPropsWithRenderFunction} from './flex'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + describe('Flex', () => { it('renders children', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).toBeInTheDocument(); }); @@ -17,7 +22,8 @@ describe('Flex', () => { render(
{props =>

Hello

}
-
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')?.tagName).toBe('P'); @@ -32,7 +38,8 @@ describe('Flex', () => { {/* @ts-expect-error - this should be a React.ElementType */} {props =>

Hello

} -
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('aria-activedescendant'); @@ -48,7 +55,8 @@ describe('Flex', () => { {/* @ts-expect-error - className is incompatible */} {props => } - + , + {wrapper: ThemeWrapper} ); }); @@ -56,23 +64,24 @@ describe('Flex', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); expectTypeOf>().toHaveProperty('htmlFor'); }); it('passes attributes to the underlying element', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByTestId('container')).toBeInTheDocument(); }); it('renders as a different element if specified', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello').tagName).toBe('SECTION'); }); it('does not bleed attributes to the underlying element', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).not.toHaveAttribute('radius'); }); @@ -80,7 +89,8 @@ describe('Flex', () => { render( Hello - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('align'); @@ -88,7 +98,7 @@ describe('Flex', () => { }); it('allows settings native html attributes', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).toHaveStyle({color: 'rgb(255, 0, 0)'}); }); @@ -97,7 +107,8 @@ describe('Flex', () => { render( Hello - + , + {wrapper: ThemeWrapper} ); expect(ref.current).toBeInTheDocument(); expect(ref.current?.tagName).toBe('OL'); @@ -112,7 +123,8 @@ describe('Flex', () => { PaddingBottom First - + , + {wrapper: ThemeWrapper} ); const paddingFirst = screen.getByText('Padding First').className; diff --git a/static/packages/scraps/src/layout/grid.spec.tsx b/static/packages/scraps/src/layout/grid.spec.tsx index e07b4a006ce0..be2db16d811b 100644 --- a/static/packages/scraps/src/layout/grid.spec.tsx +++ b/static/packages/scraps/src/layout/grid.spec.tsx @@ -1,13 +1,19 @@ import {createRef, Fragment} from 'react'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; import {expectTypeOf} from 'expect-type'; -import {render, screen} from '../../test/env'; +import {lightTheme} from '@sentry/scraps/theme'; import {Grid, type GridProps, type GridPropsWithRenderFunction} from './grid'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + describe('Grid', () => { it('renders children', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).toBeInTheDocument(); }); @@ -15,7 +21,8 @@ describe('Grid', () => { render(
{props =>

Hello

}
-
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')?.tagName).toBe('P'); @@ -30,7 +37,8 @@ describe('Grid', () => { {/* @ts-expect-error - this should be a React.ElementType */} {props =>

Hello

} -
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('aria-activedescendant'); @@ -46,22 +54,23 @@ describe('Grid', () => { {/* @ts-expect-error - className is incompatible */} {props => } - + , + {wrapper: ThemeWrapper} ); }); it('passes attributes to the underlying element', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByTestId('container')).toBeInTheDocument(); }); it('renders as a different element if specified', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello').tagName).toBe('SECTION'); }); it('does not bleed attributes to the underlying element', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).not.toHaveAttribute('radius'); }); @@ -69,7 +78,8 @@ describe('Grid', () => { render( Hello - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('align'); @@ -77,7 +87,7 @@ describe('Grid', () => { }); it('allows settings native html attributes', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).toHaveStyle({color: 'rgb(255, 0, 0)'}); }); @@ -86,7 +96,8 @@ describe('Grid', () => { render( Hello - + , + {wrapper: ThemeWrapper} ); expect(ref.current).toBeInTheDocument(); expect(ref.current?.tagName).toBe('OL'); @@ -101,7 +112,8 @@ describe('Grid', () => { PaddingBottom First - + , + {wrapper: ThemeWrapper} ); const paddingFirst = screen.getByText('Padding First').className; diff --git a/static/packages/scraps/src/layout/stack.spec.tsx b/static/packages/scraps/src/layout/stack.spec.tsx index 3fbd074ea2ba..d9e5367d0b90 100644 --- a/static/packages/scraps/src/layout/stack.spec.tsx +++ b/static/packages/scraps/src/layout/stack.spec.tsx @@ -1,13 +1,17 @@ import {createRef, Fragment} from 'react'; +import {ThemeProvider} from '@emotion/react'; +import {act, render, screen} from '@testing-library/react'; import {expectTypeOf} from 'expect-type'; import type {Responsive} from '@sentry/scraps/layout'; import {lightTheme as theme} from '@sentry/scraps/theme'; -import {act, render, screen} from '../../test/env'; - import {Stack, type StackProps, type StackPropsWithRenderFunction} from './stack'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + // A matchMedia mock that tracks listeners so tests can drive breakpoint changes. function setupTrackedMatchMedia(initialMatches: (query: string) => boolean) { const listeners: Record void>> = {}; @@ -49,7 +53,7 @@ describe('Stack', () => { }); it('renders children', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).toBeInTheDocument(); }); @@ -57,7 +61,8 @@ describe('Stack', () => { render(
{props =>

Hello

}
-
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')?.tagName).toBe('P'); @@ -70,7 +75,8 @@ describe('Stack', () => { {/* @ts-expect-error - this should be a React.ElementType */} {props =>

Hello

} -
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('aria-activedescendant'); @@ -86,22 +92,23 @@ describe('Stack', () => { {/* @ts-expect-error - className is incompatible */} {props => } - + , + {wrapper: ThemeWrapper} ); }); it('passes attributes to the underlying element', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByTestId('container')).toBeInTheDocument(); }); it('renders as a different element if specified', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello').tagName).toBe('SECTION'); }); it('does not bleed attributes to the underlying element', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).not.toHaveAttribute('radius'); }); @@ -109,7 +116,8 @@ describe('Stack', () => { render( Hello - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('align'); @@ -119,7 +127,7 @@ describe('Stack', () => { }); it('allows settings native html attributes', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello')).toHaveStyle({color: 'rgb(255, 0, 0)'}); }); @@ -127,7 +135,8 @@ describe('Stack', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); expectTypeOf>().toHaveProperty('htmlFor'); }); @@ -137,7 +146,8 @@ describe('Stack', () => { render( Hello - + , + {wrapper: ThemeWrapper} ); expect(ref.current).toBeInTheDocument(); expect(ref.current?.tagName).toBe('OL'); @@ -152,7 +162,8 @@ describe('Stack', () => { Second Stack - + , + {wrapper: ThemeWrapper} ); const firstStack = screen.getByText('First Stack').className; @@ -165,7 +176,8 @@ describe('Stack', () => {
Item 1
-
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByRole('separator')).toHaveAttribute('aria-orientation', 'vertical'); @@ -176,7 +188,8 @@ describe('Stack', () => {
Item 1
-
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByRole('separator')).toHaveAttribute( @@ -197,7 +210,8 @@ describe('Stack', () => { render( - + , + {wrapper: ThemeWrapper} ); const initialRenderCount = renderCount; @@ -218,7 +232,8 @@ describe('Stack', () => {
Item
-
+ , + {wrapper: ThemeWrapper} ); // 2xs (nothing matches) => column => horizontal separator diff --git a/static/packages/scraps/src/layout/styles.spec.tsx b/static/packages/scraps/src/layout/styles.spec.tsx index 29f0c96f627d..be1a999a635f 100644 --- a/static/packages/scraps/src/layout/styles.spec.tsx +++ b/static/packages/scraps/src/layout/styles.spec.tsx @@ -1,13 +1,12 @@ import assert from 'node:assert/strict'; -import {css} from '@emotion/react'; +import {ThemeProvider, css} from '@emotion/react'; +import {act, render, renderHook, screen} from '@testing-library/react'; import {Container} from '@sentry/scraps/layout'; import {lightTheme as theme} from '@sentry/scraps/theme'; import type {BreakpointSize} from '@sentry/scraps/theme'; -import {act, render, renderHook, screen} from '../../test/env'; - import { getBorder, rc, @@ -17,6 +16,10 @@ import { type Responsive, } from './styles'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + const normalizeCss = (value: string) => value .split('\n') @@ -204,7 +207,9 @@ describe('getBorder', () => { describe('useResponsivePropValue', () => { it('returns identity for non-responsive values', () => { - const {result} = renderHook(() => useResponsivePropValue('hello')); + const {result} = renderHook(() => useResponsivePropValue('hello'), { + wrapper: ThemeWrapper, + }); expect(result.current).toBe('hello'); }); @@ -214,8 +219,9 @@ describe('useResponsivePropValue', () => { // in the tree they resolve to the smallest defined key ('zero') — the only value // the CSS applies (the plain base declaration), so JS and CSS agree instead of // JS drifting. - const {result} = renderHook(() => - useResponsivePropValue({zero: 'base', md: 'medium'}) + const {result} = renderHook( + () => useResponsivePropValue({zero: 'base', md: 'medium'}), + {wrapper: ThemeWrapper} ); expect(result.current).toBe('base'); @@ -226,8 +232,10 @@ describe('useResponsivePropValue', () => { // reaches lg. With the viewport at lg (and no container), the viewport wins. const cleanup = setupMediaQueries({xs: true, sm: true, md: true, lg: true}); - const {result} = renderHook(() => - useResponsivePropValue({zero: 'container-base', 'screen:lg': 'viewport-large'}) + const {result} = renderHook( + () => + useResponsivePropValue({zero: 'container-base', 'screen:lg': 'viewport-large'}), + {wrapper: ThemeWrapper} ); expect(result.current).toBe('viewport-large'); @@ -237,8 +245,10 @@ describe('useResponsivePropValue', () => { it('keeps the container base when the viewport key does not match', () => { const cleanup = setupMediaQueries({xs: false, sm: false, md: false, lg: false}); - const {result} = renderHook(() => - useResponsivePropValue({zero: 'container-base', 'screen:lg': 'viewport-large'}) + const {result} = renderHook( + () => + useResponsivePropValue({zero: 'container-base', 'screen:lg': 'viewport-large'}), + {wrapper: ThemeWrapper} ); expect(result.current).toBe('container-base'); @@ -259,7 +269,9 @@ describe('useResponsivePropValue', () => { 'screen:md': 'medium', }; - const {result} = renderHook(() => useResponsivePropValue(responsiveValue)); + const {result} = renderHook(() => useResponsivePropValue(responsiveValue), { + wrapper: ThemeWrapper, + }); expect(result.current).toBe('medium'); cleanup(); @@ -277,7 +289,9 @@ describe('useResponsivePropValue', () => { 'screen:md': 'medium', }; - const {result} = renderHook(() => useResponsivePropValue(responsiveValue)); + const {result} = renderHook(() => useResponsivePropValue(responsiveValue), { + wrapper: ThemeWrapper, + }); expect(result.current).toBe('medium'); cleanup(); @@ -293,7 +307,9 @@ describe('useResponsivePropValue', () => { 'screen:sm': 'small', }; - const {result} = renderHook(() => useResponsivePropValue(responsiveValue)); + const {result} = renderHook(() => useResponsivePropValue(responsiveValue), { + wrapper: ThemeWrapper, + }); expect(result.current).toBe('small'); cleanup(); @@ -312,7 +328,9 @@ describe('useResponsivePropValue', () => { 'screen:lg': 'large', }; - const {result} = renderHook(() => useResponsivePropValue(responsiveValue)); + const {result} = renderHook(() => useResponsivePropValue(responsiveValue), { + wrapper: ThemeWrapper, + }); expect(result.current).toBe('small'); cleanup(); @@ -331,16 +349,18 @@ describe('useResponsivePropValue', () => { 'screen:lg': undefined, }; - const {result} = renderHook(() => useResponsivePropValue(responsiveValue)); + const {result} = renderHook(() => useResponsivePropValue(responsiveValue), { + wrapper: ThemeWrapper, + }); expect(result.current).toBe('medium'); cleanup(); }); it('throws an error when no breakpoints are defined in responsive prop', () => { - expect(() => renderHook(() => useResponsivePropValue({}))).toThrow( - 'Responsive prop must contain at least one breakpoint' - ); + expect(() => + renderHook(() => useResponsivePropValue({}), {wrapper: ThemeWrapper}) + ).toThrow('Responsive prop must contain at least one breakpoint'); }); }); @@ -357,7 +377,7 @@ describe('useActiveBreakpoint', () => { xl: false, }); - const {result} = renderHook(() => useActiveBreakpoint()); + const {result} = renderHook(() => useActiveBreakpoint(), {wrapper: ThemeWrapper}); expect(result.current).toBe('2xs'); cleanup(); @@ -372,7 +392,7 @@ describe('useActiveBreakpoint', () => { xl: false, }); - const {result} = renderHook(() => useActiveBreakpoint()); + const {result} = renderHook(() => useActiveBreakpoint(), {wrapper: ThemeWrapper}); expect(result.current).toBe('md'); cleanup(); @@ -382,7 +402,7 @@ describe('useActiveBreakpoint', () => { const matchMediaSpy = jest.fn(() => mockMatchMedia(false)); window.matchMedia = matchMediaSpy; - renderHook(() => useActiveBreakpoint()); + renderHook(() => useActiveBreakpoint(), {wrapper: ThemeWrapper}); // Should create media queries for all breakpoints (in reverse order) expect(matchMediaSpy).toHaveBeenCalledTimes(Object.keys(theme.breakpoints).length); @@ -405,7 +425,7 @@ describe('useActiveBreakpoint', () => { xl: true, }); - const {result} = renderHook(() => useActiveBreakpoint()); + const {result} = renderHook(() => useActiveBreakpoint(), {wrapper: ThemeWrapper}); // Should return xl (largest) when all are active expect(result.current).toBe('xl'); @@ -444,12 +464,14 @@ describe('useActiveBreakpoint', () => { return mockQuery; }); - const {result} = renderHook(() => - useResponsivePropValue({ - 'screen:xs': 'small', - 'screen:md': 'medium', - 'screen:lg': 'large', - }) + const {result} = renderHook( + () => + useResponsivePropValue({ + 'screen:xs': 'small', + 'screen:md': 'medium', + 'screen:lg': 'large', + }), + {wrapper: ThemeWrapper} ); // Initially query matches 'medium' @@ -496,8 +518,9 @@ describe('useActiveBreakpoint', () => { dispatchEvent: jest.fn(), })); - const {unmount} = renderHook(() => - useResponsivePropValue({'screen:xs': 'small', 'screen:md': 'medium'}) + const {unmount} = renderHook( + () => useResponsivePropValue({'screen:xs': 'small', 'screen:md': 'medium'}), + {wrapper: ThemeWrapper} ); // Sets up listeners for all breakpoints @@ -544,7 +567,8 @@ describe('useContainerBreakpoint', () => { render( - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('breakpoint:xl')).toBeInTheDocument(); }); @@ -554,7 +578,8 @@ describe('useContainerBreakpoint', () => { render( - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('breakpoint:zero')).toBeInTheDocument(); }); diff --git a/static/packages/scraps/src/layout/surface.spec.tsx b/static/packages/scraps/src/layout/surface.spec.tsx index c82b8d826e76..06d7a60efce9 100644 --- a/static/packages/scraps/src/layout/surface.spec.tsx +++ b/static/packages/scraps/src/layout/surface.spec.tsx @@ -1,17 +1,22 @@ import {createRef, Fragment} from 'react'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; import {Surface} from '@sentry/scraps/layout'; +import {lightTheme} from '@sentry/scraps/theme'; -import {render, screen} from '../../test/env'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} describe('Surface', () => { it('renders children', () => { - render(Hello Surface); + render(Hello Surface, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello Surface')).toBeInTheDocument(); }); it('passes attributes to the underlying element', () => { - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(screen.getByTestId('surface')).toBeInTheDocument(); }); @@ -19,7 +24,8 @@ describe('Surface', () => { render( Hello - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('variant'); expect(screen.getByText('Hello')).not.toHaveAttribute('elevation'); @@ -28,7 +34,7 @@ describe('Surface', () => { it('attaches ref to the underlying element', () => { const ref = createRef(); - render(Hello); + render(Hello, {wrapper: ThemeWrapper}); expect(ref.current).toBeInTheDocument(); expect(ref.current?.tagName).toBe('DIV'); }); @@ -42,7 +48,8 @@ describe('Surface', () => { Second - + , + {wrapper: ThemeWrapper} ); const first = screen.getByText('First').className; @@ -56,7 +63,8 @@ describe('Surface', () => { Primary Secondary Overlay - + , + {wrapper: ThemeWrapper} ); const primary = screen.getByText('Primary').className; @@ -80,7 +88,8 @@ describe('Surface', () => { High - + , + {wrapper: ThemeWrapper} ); const low = screen.getByText('Low').className; diff --git a/static/packages/scraps/src/quote/quote.tsx b/static/packages/scraps/src/quote/quote.tsx index 1b9dee57180b..fb322953b8b9 100644 --- a/static/packages/scraps/src/quote/quote.tsx +++ b/static/packages/scraps/src/quote/quote.tsx @@ -3,8 +3,7 @@ import styled from '@emotion/styled'; import {Stack} from '@sentry/scraps/layout'; import type {StackProps} from '@sentry/scraps/layout'; - -import {Text} from '../text'; +import {Text} from '@sentry/scraps/text/text'; // interface + type union because `extends` doesn't play nicely with generics interface QuoteBaseProps { diff --git a/static/packages/scraps/src/renderToString.spec.tsx b/static/packages/scraps/src/renderToString.spec.tsx new file mode 100644 index 000000000000..5856d642132b --- /dev/null +++ b/static/packages/scraps/src/renderToString.spec.tsx @@ -0,0 +1,41 @@ +import {ThemeProvider} from '@emotion/react'; +import {act, renderHook} from '@testing-library/react'; + +import {lightTheme} from '@sentry/scraps/theme'; + +import {useRenderToString} from './renderToString'; +import {Text} from './text'; + +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + +describe('renderToString', () => { + it('should render a simple component to string', async () => { + function SimpleComponent() { + return
Hello, World!
; + } + + const {result} = renderHook(useRenderToString, {wrapper: ThemeWrapper}); + + const string = await act(() => result.current()); + + expect(string).toMatchInlineSnapshot('"
Hello, World!
"'); + }); + it('should render a scraps component to string', async () => { + function ScrapsComponent() { + return ( + + SuccessText + + ); + } + + const {result} = renderHook(useRenderToString, {wrapper: ThemeWrapper}); + + const string = await act(() => result.current()); + + expect(string).toContain('data-test-id="rendered-text"'); + expect(string).toContain('SuccessText'); + }); +}); diff --git a/static/app/components/core/renderToString.tsx b/static/packages/scraps/src/renderToString.tsx similarity index 100% rename from static/app/components/core/renderToString.tsx rename to static/packages/scraps/src/renderToString.tsx diff --git a/static/app/components/core/revealOnHover/index.tsx b/static/packages/scraps/src/revealOnHover/index.tsx similarity index 100% rename from static/app/components/core/revealOnHover/index.tsx rename to static/packages/scraps/src/revealOnHover/index.tsx diff --git a/static/app/components/core/revealOnHover/revealOnHover.spec.tsx b/static/packages/scraps/src/revealOnHover/revealOnHover.spec.tsx similarity index 68% rename from static/app/components/core/revealOnHover/revealOnHover.spec.tsx rename to static/packages/scraps/src/revealOnHover/revealOnHover.spec.tsx index a0083c28c73f..f08b842c3c23 100644 --- a/static/app/components/core/revealOnHover/revealOnHover.spec.tsx +++ b/static/packages/scraps/src/revealOnHover/revealOnHover.spec.tsx @@ -1,7 +1,14 @@ -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -import {Button} from '@sentry/scraps/button'; -import {RevealOnHover} from '@sentry/scraps/revealOnHover'; +import {lightTheme} from '@sentry/scraps/theme'; + +import {RevealOnHover} from './index'; + +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} describe('RevealOnHover', () => { it('renders children and action', () => { @@ -9,9 +16,10 @@ describe('RevealOnHover', () => { Label - + - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Label')).toBeInTheDocument(); @@ -23,12 +31,14 @@ describe('RevealOnHover', () => { Label - + - + , + {wrapper: ThemeWrapper} ); const button = screen.getByRole('button', {name: 'Copy'}); + // eslint-disable-next-line testing-library/no-node-access -- The CSS reveal marker is on the action's wrapper. expect(button.closest('[data-reveal-on-hover]')).toBeInTheDocument(); }); @@ -37,12 +47,14 @@ describe('RevealOnHover', () => { Label - + - + , + {wrapper: ThemeWrapper} ); const button = screen.getByRole('button', {name: 'Copy'}); + // eslint-disable-next-line testing-library/no-node-access -- The CSS reveal marker is on the action's wrapper. expect(button.closest('[data-reveal-on-hover]')).toHaveAttribute( 'data-reveal-on-hover-visible', '' @@ -54,9 +66,10 @@ describe('RevealOnHover', () => { Label - + - + , + {wrapper: ThemeWrapper} ); const button = screen.getByRole('button', {name: 'Copy'}); @@ -65,7 +78,7 @@ describe('RevealOnHover', () => { Label - + ); @@ -78,9 +91,10 @@ describe('RevealOnHover', () => { Label - + - + , + {wrapper: ThemeWrapper} ); expect(screen.getByTestId('hover-root')).toBeInTheDocument(); @@ -93,9 +107,10 @@ describe('RevealOnHover', () => { Label - + - + , + {wrapper: ThemeWrapper} ); await userEvent.click(screen.getByRole('button', {name: 'Copy'})); @@ -107,17 +122,20 @@ describe('RevealOnHover', () => { Label - + - + - + , + {wrapper: ThemeWrapper} ); const copyButton = screen.getByRole('button', {name: 'Copy'}); const deleteButton = screen.getByRole('button', {name: 'Delete'}); + // eslint-disable-next-line testing-library/no-node-access -- The CSS reveal marker is on the action's wrapper. expect(copyButton.closest('[data-reveal-on-hover]')).toBeInTheDocument(); + // eslint-disable-next-line testing-library/no-node-access -- The CSS reveal marker is on the action's wrapper. expect(deleteButton.closest('[data-reveal-on-hover]')).toBeInTheDocument(); }); @@ -126,9 +144,10 @@ describe('RevealOnHover', () => { Label - + - + , + {wrapper: ThemeWrapper} ); await userEvent.tab(); @@ -142,16 +161,18 @@ describe('RevealOnHover', () => {
Grid content - +
)} - + , + {wrapper: ThemeWrapper} ); expect(screen.getByTestId('custom-root')).toBeInTheDocument(); expect(screen.getByText('Grid content')).toBeInTheDocument(); const button = screen.getByRole('button', {name: 'Copy'}); + // eslint-disable-next-line testing-library/no-node-access -- The CSS reveal marker is on the action's wrapper. expect(button.closest('[data-reveal-on-hover]')).toBeInTheDocument(); }); @@ -164,11 +185,12 @@ describe('RevealOnHover', () => {
Content - +
)} - + , + {wrapper: ThemeWrapper} ); await userEvent.click(screen.getByRole('button', {name: 'Copy'})); diff --git a/static/app/components/core/revealOnHover/revealOnHover.tsx b/static/packages/scraps/src/revealOnHover/revealOnHover.tsx similarity index 100% rename from static/app/components/core/revealOnHover/revealOnHover.tsx rename to static/packages/scraps/src/revealOnHover/revealOnHover.tsx diff --git a/static/packages/scraps/src/separator/separator.spec.tsx b/static/packages/scraps/src/separator/separator.spec.tsx index d360bb814107..6e76428f37c2 100644 --- a/static/packages/scraps/src/separator/separator.spec.tsx +++ b/static/packages/scraps/src/separator/separator.spec.tsx @@ -1,15 +1,21 @@ +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; + import {Separator} from '@sentry/scraps/separator'; +import {lightTheme} from '@sentry/scraps/theme'; -import {render, screen} from '../../test/env'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} describe('Separator', () => { it('should render a horizontal Separator', () => { - render(); + render(, {wrapper: ThemeWrapper}); expect(screen.getByRole('separator')).toBeInTheDocument(); }); it('should render a vertical Separator', () => { - render(); + render(, {wrapper: ThemeWrapper}); expect(screen.getByRole('separator')).toBeInTheDocument(); }); @@ -17,7 +23,8 @@ describe('Separator', () => { expect(() => render( // @ts-expect-error children are not allowed - Hello + Hello, + {wrapper: ThemeWrapper} ) ).toThrow(); }); diff --git a/static/packages/scraps/src/separator/separator.tsx b/static/packages/scraps/src/separator/separator.tsx index c4241ef9aff9..8c95699f1c61 100644 --- a/static/packages/scraps/src/separator/separator.tsx +++ b/static/packages/scraps/src/separator/separator.tsx @@ -1,8 +1,8 @@ import isPropValid from '@emotion/is-prop-valid'; import styled from '@emotion/styled'; -import type {ContainerProps} from '../layout/container'; -import {getBorder, getMargin, getSpacing, rc} from '../layout/styles'; +import type {ContainerProps} from '@sentry/scraps/layout/container'; +import {getBorder, getMargin, getSpacing, rc} from '@sentry/scraps/layout/styles'; export type SeparatorProps = Pick & { orientation: 'horizontal' | 'vertical'; diff --git a/static/app/components/core/sizeContext.tsx b/static/packages/scraps/src/sizeContext.tsx similarity index 100% rename from static/app/components/core/sizeContext.tsx rename to static/packages/scraps/src/sizeContext.tsx diff --git a/static/app/components/core/slider/index.tsx b/static/packages/scraps/src/slider/index.tsx similarity index 100% rename from static/app/components/core/slider/index.tsx rename to static/packages/scraps/src/slider/index.tsx diff --git a/static/app/components/core/slider/slider.spec.tsx b/static/packages/scraps/src/slider/slider.spec.tsx similarity index 79% rename from static/app/components/core/slider/slider.spec.tsx rename to static/packages/scraps/src/slider/slider.spec.tsx index 419494e7171e..91bb01f0f012 100644 --- a/static/app/components/core/slider/slider.spec.tsx +++ b/static/packages/scraps/src/slider/slider.spec.tsx @@ -1,8 +1,15 @@ import {useState} from 'react'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {lightTheme} from '@sentry/scraps/theme'; -import {Slider} from '@sentry/scraps/slider'; +import {Slider} from './index'; + +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} function ControlledSlider( props: Omit, 'value' | 'onChange'> & { @@ -17,22 +24,26 @@ function ControlledSlider( describe('Slider', () => { describe('rendering', () => { it('renders with default value (uncontrolled)', () => { - render(); + render(, {wrapper: ThemeWrapper}); expect(screen.getByRole('slider')).toHaveValue('50'); }); it('renders with controlled value', () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); expect(screen.getByRole('slider')).toHaveValue('75'); }); it('applies aria-label', () => { - render(); + render(, {wrapper: ThemeWrapper}); expect(screen.getByRole('slider')).toHaveAccessibleName('Volume'); }); it('sets min/max/step attributes on the input', () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); const slider = screen.getByRole('slider'); expect(slider).toHaveAttribute('min', '10'); expect(slider).toHaveAttribute('max', '90'); @@ -40,21 +51,25 @@ describe('Slider', () => { }); it('sets aria-valuetext', () => { - render(); + render(, {wrapper: ThemeWrapper}); expect(screen.getByRole('slider')).toHaveAttribute('aria-valuetext', '30'); }); }); describe('disabled', () => { it('disables the slider', () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); expect(screen.getByRole('slider')).toBeDisabled(); }); }); describe('keyboard interaction', () => { it('increases value with ArrowRight', async () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); const slider = screen.getByRole('slider'); await userEvent.tab(); expect(slider).toHaveFocus(); @@ -63,42 +78,54 @@ describe('Slider', () => { }); it('decreases value with ArrowLeft', async () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); await userEvent.tab(); await userEvent.keyboard('{ArrowLeft}'); expect(screen.getByRole('slider')).toHaveValue('49'); }); it('jumps to min with Home', async () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); await userEvent.tab(); await userEvent.keyboard('{Home}'); expect(screen.getByRole('slider')).toHaveValue('10'); }); it('jumps to max with End', async () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); await userEvent.tab(); await userEvent.keyboard('{End}'); expect(screen.getByRole('slider')).toHaveValue('90'); }); it('respects step when using arrow keys', async () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); await userEvent.tab(); await userEvent.keyboard('{ArrowRight}'); expect(screen.getByRole('slider')).toHaveValue('60'); }); it('does not exceed max', async () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); await userEvent.tab(); await userEvent.keyboard('{ArrowRight}{ArrowRight}{ArrowRight}'); expect(screen.getByRole('slider')).toHaveValue('100'); }); it('does not go below min', async () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); await userEvent.tab(); await userEvent.keyboard('{ArrowLeft}{ArrowLeft}{ArrowLeft}'); expect(screen.getByRole('slider')).toHaveValue('0'); @@ -108,7 +135,8 @@ describe('Slider', () => { describe('ticks', () => { it('renders tick labels with count', () => { render( - + , + {wrapper: ThemeWrapper} ); // count: 3 at 0, 50, 100. Value is 30, so "50" is unique to tick labels. expect(screen.getByText('50')).toBeInTheDocument(); @@ -120,7 +148,8 @@ describe('Slider', () => { defaultValue={50} ticks={{values: [0, 25, 75, 100], labels: true}} aria-label="Test" - /> + />, + {wrapper: ThemeWrapper} ); // 25 and 75 only appear as tick labels (not as value label or edge labels) expect(screen.getByText('25')).toBeInTheDocument(); @@ -133,7 +162,8 @@ describe('Slider', () => { defaultValue={25} ticks={{interval: 50, labels: true}} aria-label="Test" - /> + />, + {wrapper: ThemeWrapper} ); // Tick at 50 is unique (value is 25, edges are 0/100) expect(screen.getByText('50')).toBeInTheDocument(); @@ -147,7 +177,8 @@ describe('Slider', () => { defaultValue={42} formatOptions={{style: 'unit', unit: 'percent'}} aria-label="Test" - /> + />, + {wrapper: ThemeWrapper} ); // 42% only appears in the value label (not in edge labels since 42 != min or max) expect(screen.getByText('42%')).toBeInTheDocument(); @@ -160,7 +191,8 @@ describe('Slider', () => { ticks={{count: 3, labels: true}} formatOptions={{style: 'currency', currency: 'USD', maximumFractionDigits: 0}} aria-label="Test" - /> + />, + {wrapper: ThemeWrapper} ); // "$50" only appears in tick labels (value is 25, and 50 is a tick but not the value) expect(screen.getByText('$50')).toBeInTheDocument(); @@ -169,7 +201,9 @@ describe('Slider', () => { describe('name prop', () => { it('sets name on the hidden input', () => { - render(); + render(, { + wrapper: ThemeWrapper, + }); expect(screen.getByRole('slider')).toHaveAttribute('name', 'volume'); }); }); @@ -177,7 +211,9 @@ describe('Slider', () => { describe('value as empty string', () => { it('treats empty string value as uncontrolled', () => { const onChange = jest.fn(); - render(); + render(, { + wrapper: ThemeWrapper, + }); expect(screen.getByRole('slider')).toBeInTheDocument(); }); }); diff --git a/static/app/components/core/slider/slider.tsx b/static/packages/scraps/src/slider/slider.tsx similarity index 100% rename from static/app/components/core/slider/slider.tsx rename to static/packages/scraps/src/slider/slider.tsx diff --git a/static/app/components/core/slot/index.tsx b/static/packages/scraps/src/slot/index.tsx similarity index 100% rename from static/app/components/core/slot/index.tsx rename to static/packages/scraps/src/slot/index.tsx diff --git a/static/app/components/core/slot/knownContexts.ts b/static/packages/scraps/src/slot/knownContexts.ts similarity index 100% rename from static/app/components/core/slot/knownContexts.ts rename to static/packages/scraps/src/slot/knownContexts.ts diff --git a/static/app/components/core/slot/slot.spec.tsx b/static/packages/scraps/src/slot/slot.spec.tsx similarity index 99% rename from static/app/components/core/slot/slot.spec.tsx rename to static/packages/scraps/src/slot/slot.spec.tsx index af7f61073054..2171d753f6b2 100644 --- a/static/app/components/core/slot/slot.spec.tsx +++ b/static/packages/scraps/src/slot/slot.spec.tsx @@ -1,8 +1,8 @@ -import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {render, screen} from '@testing-library/react'; import {SizeProvider, useSizeContext} from '@sentry/scraps/sizeContext'; -import {slot} from './'; +import {slot} from './index'; describe('slot', () => { it('returns a module with Provider, Outlet, and Fallback sub-components', () => { diff --git a/static/app/components/core/slot/slot.tsx b/static/packages/scraps/src/slot/slot.tsx similarity index 99% rename from static/app/components/core/slot/slot.tsx rename to static/packages/scraps/src/slot/slot.tsx index 621abc0fcbd7..b156e5f643c9 100644 --- a/static/app/components/core/slot/slot.tsx +++ b/static/packages/scraps/src/slot/slot.tsx @@ -1,3 +1,4 @@ +import type * as React from 'react'; import { createContext, use, diff --git a/static/app/components/core/switch/index.tsx b/static/packages/scraps/src/switch/index.tsx similarity index 100% rename from static/app/components/core/switch/index.tsx rename to static/packages/scraps/src/switch/index.tsx diff --git a/static/packages/scraps/src/switch/switch.spec.tsx b/static/packages/scraps/src/switch/switch.spec.tsx new file mode 100644 index 000000000000..04240e03ce3f --- /dev/null +++ b/static/packages/scraps/src/switch/switch.spec.tsx @@ -0,0 +1,36 @@ +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import {lightTheme} from '@sentry/scraps/theme'; + +import {Switch} from './index'; + +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + +describe('Switch', () => { + it('disabled', () => { + render(, {wrapper: ThemeWrapper}); + expect(screen.getByRole('checkbox')).toBeDisabled(); + }); + + it('checked', () => { + render(, {wrapper: ThemeWrapper}); + expect(screen.getByRole('checkbox')).toBeChecked(); + }); + + it('disallows toggling a disabled switch', async () => { + const onClick = jest.fn(); + + render(, {wrapper: ThemeWrapper}); + + const switchButton = screen.getByRole('checkbox'); + expect(switchButton).not.toBeChecked(); + + await userEvent.click(switchButton); + expect(switchButton).not.toBeChecked(); + expect(onClick).not.toHaveBeenCalled(); + }); +}); diff --git a/static/app/components/core/switch/switch.tsx b/static/packages/scraps/src/switch/switch.tsx similarity index 100% rename from static/app/components/core/switch/switch.tsx rename to static/packages/scraps/src/switch/switch.tsx diff --git a/static/packages/scraps/src/text/heading.spec.tsx b/static/packages/scraps/src/text/heading.spec.tsx index 835e2f3b8303..c45edb0cdcd8 100644 --- a/static/packages/scraps/src/text/heading.spec.tsx +++ b/static/packages/scraps/src/text/heading.spec.tsx @@ -1,16 +1,23 @@ import {createRef} from 'react'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; import {expectTypeOf} from 'expect-type'; -import {render, screen} from '../../test/env'; +import {lightTheme} from '@sentry/scraps/theme'; import {Heading, type HeadingProps, type HeadingPropsWithRenderFunction} from './heading'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + describe('Heading', () => { it('renders with correct HTML element', () => { render( Heading 6 - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Heading 6').tagName).toBe('H6'); }); @@ -19,7 +26,8 @@ describe('Heading', () => { render( Heading 6 - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Heading 6')).not.toHaveAttribute('align'); }); @@ -28,7 +36,8 @@ describe('Heading', () => { render( Heading 6 - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Heading 6')).toHaveAttribute('data-test-id', 'test-id'); }); @@ -36,7 +45,8 @@ describe('Heading', () => { render( Heading 6 - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Heading 6')).toHaveStyle({color: 'rgb(255, 0, 0)'}); }); @@ -45,7 +55,8 @@ describe('Heading', () => { render( Heading 6 - + , + {wrapper: ThemeWrapper} ); expect(ref.current?.tagName).toBe('H6'); }); @@ -54,7 +65,8 @@ describe('Heading', () => { render(
{props =>

Title

}
-
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Title')?.tagName).toBe('H2'); @@ -69,7 +81,8 @@ describe('Heading', () => { {/* @ts-expect-error - this should be a React.ElementType */} {props =>

Title

} -
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Title')).not.toHaveAttribute('aria-activedescendant'); @@ -85,7 +98,8 @@ describe('Heading', () => { {/* @ts-expect-error - className is incompatible */} {props => } - + , + {wrapper: ThemeWrapper} ); }); @@ -94,7 +108,8 @@ describe('Heading', () => { // @ts-expect-error: Heading does not support the display prop Title - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Title').tagName).toBe('H1'); }); diff --git a/static/packages/scraps/src/text/text.spec.tsx b/static/packages/scraps/src/text/text.spec.tsx index 7b073126bd5f..d804a7d97ba6 100644 --- a/static/packages/scraps/src/text/text.spec.tsx +++ b/static/packages/scraps/src/text/text.spec.tsx @@ -1,13 +1,42 @@ import {createRef, Fragment} from 'react'; +import {ThemeProvider} from '@emotion/react'; +import {render, screen} from '@testing-library/react'; import {expectTypeOf} from 'expect-type'; import {lightTheme as theme} from '@sentry/scraps/theme'; -import {render, screen} from '../../test/env'; -import {getEmotionRules} from '../../test/env/emotion'; - import {Text, type TextProps, type TextPropsWithRenderFunction} from './index'; +function ThemeWrapper({children}: {children: React.ReactNode}) { + return {children}; +} + +/** + * Emotion injects its styles through the CSSOM (`insertRule`), so they are not + * reflected in `getComputedStyle` under jsdom. Read the generated rules for an + * element directly off `document.styleSheets` instead. + */ +function getEmotionRules(element: HTMLElement): string[] { + const classes = element.className.split(' ').filter(cls => cls.startsWith('css-')); + const rules: string[] = []; + for (const sheet of Array.from(document.styleSheets)) { + let sheetRules: CSSRuleList; + try { + sheetRules = sheet.cssRules; + } catch { + continue; + } + for (const rule of Array.from(sheetRules)) { + if (classes.some(cls => rule.cssText.includes(cls))) { + // CSSOM formatting varies between DOM implementations. Tests care about + // the generated declarations, not whether an at-rule contains newlines. + rules.push(rule.cssText.replace(/\s+/g, ' ')); + } + } + } + return rules; +} + /** The `display` value of the always-applied base declaration (no at-rule). */ function getBaseDisplay(element: HTMLElement): string | undefined { const base = getEmotionRules(element).find(rule => rule.trimStart().startsWith('.')); @@ -16,23 +45,23 @@ function getBaseDisplay(element: HTMLElement): string | undefined { describe('Text', () => { it('Defaults to span', () => { - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello World').tagName).toBe('SPAN'); }); it('renders with p as HTML element', () => { - render(Paragraph text); + render(Paragraph text, {wrapper: ThemeWrapper}); expect(screen.getByText('Paragraph text').tagName).toBe('P'); }); it('does not bleed props to the DOM element', () => { - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello World')).not.toHaveAttribute('align'); }); it('forwards data-test-id', () => { - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); expect(screen.getByText('Hello World')).toHaveAttribute('data-test-id', 'test-id'); }); @@ -44,7 +73,8 @@ describe('Text', () => { Hello World - + , + {wrapper: ThemeWrapper} ); expectTypeOf>().toHaveProperty('htmlFor'); }); @@ -53,7 +83,8 @@ describe('Text', () => { render( Paragraph text - + , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Paragraph text')).toHaveStyle({color: 'rgb(255, 0, 0)'}); }); @@ -63,21 +94,23 @@ describe('Text', () => { render( Paragraph text - + , + {wrapper: ThemeWrapper} ); expect(ref.current?.tagName).toBe('P'); }); it('does not allow color prop', () => { // @ts-expect-error: color is not a valid prop for Text - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); }); it('implements render prop', () => { render(
{props =>

Hello

}
-
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')?.tagName).toBe('P'); @@ -92,7 +125,8 @@ describe('Text', () => { {/* @ts-expect-error - this should be a React.ElementType */} {props =>

Hello

} -
+ , + {wrapper: ThemeWrapper} ); expect(screen.getByText('Hello')).not.toHaveAttribute('aria-activedescendant'); @@ -108,23 +142,24 @@ describe('Text', () => { {/* @ts-expect-error - className is incompatible */} {props => } - + , + {wrapper: ThemeWrapper} ); }); describe('display', () => { it('emits no display for a plain span', () => { - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); expect(getBaseDisplay(screen.getByText('Hello World'))).toBeUndefined(); }); it('defaults to block when as="div"', () => { - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); expect(getBaseDisplay(screen.getByText('Hello World'))).toBe('block'); }); it('forces block for ellipsis', () => { - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); expect(getBaseDisplay(screen.getByText('Hello World'))).toBe('block'); }); @@ -132,13 +167,14 @@ describe('Text', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); expect(getBaseDisplay(screen.getByText('Hello World'))).toBe('inline-block'); }); it('applies a scalar display prop', () => { - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); expect(getBaseDisplay(screen.getByText('Hello World'))).toBe('none'); }); @@ -146,7 +182,8 @@ describe('Text', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); expect(getBaseDisplay(screen.getByText('Hello World'))).toBe('inline'); }); @@ -155,7 +192,8 @@ describe('Text', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); const element = screen.getByText('Hello World'); // The default (block) fills the base so the div is not hidden below md... @@ -172,7 +210,8 @@ describe('Text', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); const element = screen.getByText('Hello World'); // Without a derived default, the element's native display (block for

) @@ -187,7 +226,7 @@ describe('Text', () => { }); it('seeds the base with the native inline display for a responsive span', () => { - render(Hello World); + render(Hello World, {wrapper: ThemeWrapper}); // A span is inline by default, so it stays visible below md instead of // inheriting the `none` from the smallest specified breakpoint. expect(getBaseDisplay(screen.getByText('Hello World'))).toBe('inline'); @@ -197,7 +236,8 @@ describe('Text', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); expect(getBaseDisplay(screen.getByText('Hello World'))).toBe('none'); }); @@ -206,7 +246,8 @@ describe('Text', () => { render( Hello World - + , + {wrapper: ThemeWrapper} ); // align needs a block-level box; the derived block seeds the base. expect(getBaseDisplay(screen.getByText('Hello World'))).toBe('block'); @@ -217,7 +258,8 @@ describe('Text', () => { // @ts-expect-error: display cannot be combined with ellipsis Hello World - + , + {wrapper: ThemeWrapper} ); }); }); diff --git a/static/packages/scraps/src/theme/index.ts b/static/packages/scraps/src/theme/index.ts index 8ef3fb84ec6e..52ec6c667428 100644 --- a/static/packages/scraps/src/theme/index.ts +++ b/static/packages/scraps/src/theme/index.ts @@ -1,15 +1,5 @@ -import type {darkTheme} from './dark'; -import type {lightTheme} from './light'; - -export {lightTheme} from './light'; -export {darkTheme} from './dark'; +export {lightTheme, darkTheme} from './theme'; +export type {SentryTheme as Theme, StrictCSSObject} from './theme'; export {baseTheme} from './base'; export type * from './types'; -export type Theme = { - [Key in keyof typeof lightTheme]: (typeof lightTheme)[Key] | (typeof darkTheme)[Key]; -}; -type ScrapsTheme = Theme; - -declare module '@emotion/react' { - export interface Theme extends ScrapsTheme {} -} +export {useInvertedTheme} from './useInvertedTheme'; diff --git a/static/app/utils/theme/swatch.spec.tsx b/static/packages/scraps/src/theme/swatch.spec.tsx similarity index 98% rename from static/app/utils/theme/swatch.spec.tsx rename to static/packages/scraps/src/theme/swatch.spec.tsx index 4b327f016d1c..ddf0516ab577 100644 --- a/static/app/utils/theme/swatch.spec.tsx +++ b/static/packages/scraps/src/theme/swatch.spec.tsx @@ -1,4 +1,4 @@ -import {makeSwatch} from 'sentry/utils/theme/swatch'; +import {makeSwatch} from './swatch'; const TEST_PALETTE = { blurple: '#7553FF', diff --git a/static/app/utils/theme/swatch.tsx b/static/packages/scraps/src/theme/swatch.tsx similarity index 87% rename from static/app/utils/theme/swatch.tsx rename to static/packages/scraps/src/theme/swatch.tsx index ca78eff5f4d9..4110b4fd2168 100644 --- a/static/app/utils/theme/swatch.tsx +++ b/static/packages/scraps/src/theme/swatch.tsx @@ -31,6 +31,7 @@ export function makeSwatch(palette: Record, onVibrant: OnVibrant const entries = Object.entries(palette); const swatchColors = entries.map(([key, hex]) => ({ background: hex, + // oxlint-disable-next-line typescript/no-non-null-assertion -- Categorical palettes use the keys in this contrast map. content: onVibrant[SWATCH_CONTRAST_MAP[key]!], })); const colorSet: ReadonlySet = Object.freeze( @@ -47,6 +48,7 @@ export function makeSwatch(palette: Record, onVibrant: OnVibrant */ get(input: string): {background: string; content: string} { const index = hashIdentifier(input ?? '') % swatchColors.length; + // oxlint-disable-next-line typescript/no-non-null-assertion -- Modulo selects an entry from the non-empty categorical palette. return {...swatchColors[index]!}; }, diff --git a/static/packages/scraps/src/theme/theme.spec.tsx b/static/packages/scraps/src/theme/theme.spec.tsx new file mode 100644 index 000000000000..51e40b2b64e2 --- /dev/null +++ b/static/packages/scraps/src/theme/theme.spec.tsx @@ -0,0 +1,77 @@ +import {expectTypeOf} from 'expect-type'; + +import {darkTheme, lightTheme} from './index'; + +describe.each([lightTheme, darkTheme])('$type theme', theme => { + describe('getColorPalette', () => { + it('should return correct amount of colors', () => { + expect(theme.chart.getColorPalette(2)).toHaveLength(3); + }); + + it('should return all colors when all is passed', () => { + const colors = theme.chart.getColorPalette('all'); + + expect(colors).toHaveLength(18); + }); + + it('should have strict types', () => { + const colors = theme.chart.getColorPalette(2); + + expectTypeOf(colors).toEqualTypeOf< + | readonly ['#7553FF', '#3A1873', '#F0369A'] + | readonly ['#7553FF', '#50219C', '#F0369A'] + >(); + }); + }); + + describe('swatch', () => { + it('should match old getColorPalette(9) colors for backward compatibility', () => { + const oldPalette = theme.chart.getColorPalette(9); + const swatchColors = new Set(theme.swatch.values()); + + expect(swatchColors.size).toBe(10); + expect(swatchColors.size).toBe(oldPalette.length); + + for (const color of oldPalette) { + expect(swatchColors.has(color)).toBe(true); + } + }); + }); + + it('provides focus styles', () => { + expect(theme.focusRing()).toEqual({ + outline: 'none', + boxShadow: `0 0 0 0 ${theme.tokens.background.primary}, 0 0 0 2px ${theme.tokens.focus.default}`, + }); + }); +}); + +it.each([ + ['production', undefined, false], + ['development', undefined, false], + ['test', undefined, true], + ['production', '1', true], + ['development', '1', true], +] as const)( + 'configures motion for NODE_ENV=%s and IS_ACCEPTANCE_TEST=%s', + async (nodeEnv, acceptanceTest, disabled) => { + const environment = jest.replaceProperty(process, 'env', { + ...process.env, + NODE_ENV: nodeEnv, + IS_ACCEPTANCE_TEST: acceptanceTest, + }); + + try { + await jest.isolateModulesAsync(async () => { + const {darkTheme, lightTheme} = await import('./theme'); + + for (const theme of [lightTheme, darkTheme]) { + expect(theme.motion.framer.smooth.fast.duration).toBe(disabled ? 0 : 0.12); + expect(theme.motion.smooth.fast).toBe('120ms cubic-bezier(0.72, 0, 0.16, 1)'); + } + }); + } finally { + environment.restore(); + } + } +); diff --git a/static/app/utils/theme/theme.tsx b/static/packages/scraps/src/theme/theme.tsx similarity index 98% rename from static/app/utils/theme/theme.tsx rename to static/packages/scraps/src/theme/theme.tsx index f34d59fa46c7..bbae07efa5a2 100644 --- a/static/app/utils/theme/theme.tsx +++ b/static/packages/scraps/src/theme/theme.tsx @@ -11,18 +11,12 @@ import type {CSSProperties} from 'react'; import {css} from '@emotion/react'; import {spring, type Transition} from 'framer-motion'; -import { - darkTheme as baseDarkTheme, - lightTheme as baseLightTheme, - type Theme as ScrapsTheme, - type MotionDuration, - type MotionEasing, -} from '@sentry/scraps/theme'; import {color, typography} from '@sentry/scraps/tokens'; -import {IS_ACCEPTANCE_TEST, NODE_ENV} from 'sentry/constants/env'; - +import {darkTheme as baseDarkTheme} from './dark'; +import {lightTheme as baseLightTheme} from './light'; import {makeSwatch, type Swatch} from './swatch'; +import type {MotionDuration, MotionEasing} from './types'; type MotionDefinition = Record; @@ -43,7 +37,8 @@ const motionCurves: Record< }; // Disable transitions in acceptance tests or node testing environments. -const IS_ACCEPTANCE_OR_TESTING = IS_ACCEPTANCE_TEST || NODE_ENV === 'test'; +const IS_ACCEPTANCE_OR_TESTING = + !!process.env.IS_ACCEPTANCE_TEST || process.env.NODE_ENV === 'test'; const EMPTY_TRANSITION: Transition = { duration: 0, staggerChildren: 0, @@ -259,6 +254,12 @@ const commonTheme = { ...typography, } as const; +type ScrapsTheme = { + [Key in keyof typeof baseLightTheme]: + | (typeof baseLightTheme)[Key] + | (typeof baseDarkTheme)[Key]; +}; + export interface SentryTheme extends ScrapsTheme, Omit { chart: { diff --git a/static/packages/scraps/src/theme/useInvertedTheme.spec.tsx b/static/packages/scraps/src/theme/useInvertedTheme.spec.tsx new file mode 100644 index 000000000000..7f0e632d3327 --- /dev/null +++ b/static/packages/scraps/src/theme/useInvertedTheme.spec.tsx @@ -0,0 +1,17 @@ +import {ThemeProvider} from '@emotion/react'; +import {renderHook} from '@testing-library/react'; + +import {darkTheme, lightTheme, useInvertedTheme} from './index'; + +it.each([ + [lightTheme, darkTheme], + [darkTheme, lightTheme], +])('returns the opposite theme', (theme, oppositeTheme) => { + function Wrapper({children}: {children: React.ReactNode}) { + return {children}; + } + + const {result} = renderHook(useInvertedTheme, {wrapper: Wrapper}); + + expect(result.current).toBe(oppositeTheme); +}); diff --git a/static/app/utils/theme/useInvertedTheme.ts b/static/packages/scraps/src/theme/useInvertedTheme.ts similarity index 100% rename from static/app/utils/theme/useInvertedTheme.ts rename to static/packages/scraps/src/theme/useInvertedTheme.ts diff --git a/static/app/components/core/translation/index.ts b/static/packages/scraps/src/translation/index.ts similarity index 80% rename from static/app/components/core/translation/index.ts rename to static/packages/scraps/src/translation/index.ts index 0af531a9465a..9d49ffa103f4 100644 --- a/static/app/components/core/translation/index.ts +++ b/static/packages/scraps/src/translation/index.ts @@ -4,3 +4,5 @@ import {TranslationContext} from './translationContext'; export type {TranslationContextValue} from './translationContext'; export const TranslationContextProvider = TranslationContext.Provider; + +export {useTranslation} from './useTranslation'; diff --git a/static/app/components/core/translation/translationContext.tsx b/static/packages/scraps/src/translation/translationContext.tsx similarity index 100% rename from static/app/components/core/translation/translationContext.tsx rename to static/packages/scraps/src/translation/translationContext.tsx diff --git a/static/app/components/core/translation/useTranslation.ts b/static/packages/scraps/src/translation/useTranslation.ts similarity index 100% rename from static/app/components/core/translation/useTranslation.ts rename to static/packages/scraps/src/translation/useTranslation.ts diff --git a/static/app/components/core/useScrollLock.spec.tsx b/static/packages/scraps/src/useScrollLock.spec.tsx similarity index 91% rename from static/app/components/core/useScrollLock.spec.tsx rename to static/packages/scraps/src/useScrollLock.spec.tsx index ce2432e7ca4e..9760c7523d95 100644 --- a/static/app/components/core/useScrollLock.spec.tsx +++ b/static/packages/scraps/src/useScrollLock.spec.tsx @@ -1,4 +1,4 @@ -import {renderHook} from 'sentry-test/reactTestingLibrary'; +import {renderHook} from '@testing-library/react'; import {useScrollLock} from './useScrollLock'; @@ -7,14 +7,20 @@ describe('useScrollLock', () => { beforeEach(() => { jest.useFakeTimers(); + jest.spyOn(window, 'scrollTo').mockImplementation(() => {}); container = document.createElement('div'); container.style.overflow = 'auto'; document.body.appendChild(container); }); afterEach(() => { - jest.useRealTimers(); - document.body.removeChild(container); + try { + jest.runOnlyPendingTimers(); + } finally { + jest.useRealTimers(); + document.body.removeChild(container); + jest.restoreAllMocks(); + } }); it('locks scroll when acquired', () => { @@ -193,12 +199,12 @@ describe('useScrollLock', () => { result.current.release(); - expect(document.body).toHaveStyle({position: ''}); - expect(document.body).toHaveStyle({top: ''}); - expect(document.body).toHaveStyle({left: ''}); - expect(document.body).toHaveStyle({right: ''}); - expect(document.body).toHaveStyle({width: ''}); - expect(document.body).toHaveStyle({paddingRight: ''}); + expect(document.body.style.getPropertyValue('position')).toBe(''); + expect(document.body.style.getPropertyValue('top')).toBe(''); + expect(document.body.style.getPropertyValue('left')).toBe(''); + expect(document.body.style.getPropertyValue('right')).toBe(''); + expect(document.body.style.getPropertyValue('width')).toBe(''); + expect(document.body.style.getPropertyValue('padding-right')).toBe(''); jest.runAllTimers(); expect(scrollToSpy).toHaveBeenCalledWith(scrollX, scrollY); diff --git a/static/app/components/core/useScrollLock.tsx b/static/packages/scraps/src/useScrollLock.tsx similarity index 100% rename from static/app/components/core/useScrollLock.tsx rename to static/packages/scraps/src/useScrollLock.tsx diff --git a/static/packages/scraps/test/env/emotion.ts b/static/packages/scraps/test/env/emotion.ts deleted file mode 100644 index 01ebf04494df..000000000000 --- a/static/packages/scraps/test/env/emotion.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Emotion injects its styles through the CSSOM (`insertRule`), so they are not - * reflected in `getComputedStyle` under jsdom. Read the generated rules for an - * element directly off `document.styleSheets` instead. - */ -export function getEmotionRules(element: HTMLElement): string[] { - const classes = element.className.split(' ').filter(cls => cls.startsWith('css-')); - const rules: string[] = []; - for (const sheet of Array.from(document.styleSheets)) { - let sheetRules: CSSRuleList; - try { - sheetRules = sheet.cssRules; - } catch { - continue; - } - for (const rule of Array.from(sheetRules)) { - if (classes.some(cls => rule.cssText.includes(cls))) { - // CSSOM formatting varies between DOM implementations. Tests care about - // the generated declarations, not whether an at-rule contains newlines. - rules.push(rule.cssText.replace(/\s+/g, ' ')); - } - } - } - return rules; -} diff --git a/static/packages/scraps/test/env/index.tsx b/static/packages/scraps/test/env/index.tsx deleted file mode 100644 index b5d9de71996b..000000000000 --- a/static/packages/scraps/test/env/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type {ReactNode} from 'react'; -import {ThemeProvider} from '@emotion/react'; -import { - render as rtlRender, - renderHook as rtlRenderHook, - type RenderHookOptions, - type RenderOptions, - type RenderResult, -} from '@testing-library/react'; - -import {lightTheme} from '@sentry/scraps/theme'; - -export {act, screen} from '@testing-library/react'; - -function ThemeWrapper({children}: {children: ReactNode}) { - return {children}; -} - -export function render(ui: ReactNode, options?: RenderOptions): RenderResult { - return rtlRender(ui, {wrapper: ThemeWrapper, ...options}); -} - -export function renderHook( - callback: (props: Props) => Result, - options?: RenderHookOptions -) { - return rtlRenderHook(callback, {wrapper: ThemeWrapper, ...options}); -} diff --git a/static/packages/scraps/test/env/setup.ts b/static/packages/scraps/test/env/setup.ts index 10c80a2846e3..c39752cc67be 100644 --- a/static/packages/scraps/test/env/setup.ts +++ b/static/packages/scraps/test/env/setup.ts @@ -3,3 +3,10 @@ import '@testing-library/jest-dom'; import {configure} from '@testing-library/react'; configure({testIdAttribute: 'data-test-id'}); + +// jsdom does not implement ResizeObserver. Layout tests can supply measured sizes. +globalThis.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; diff --git a/static/packages/scraps/tsconfig.json b/static/packages/scraps/tsconfig.json index c1b683e01508..80d8b7996cce 100644 --- a/static/packages/scraps/tsconfig.json +++ b/static/packages/scraps/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "lib": ["ES2022", "DOM", "DOM.Iterable"], + "lib": ["ES2023", "DOM", "DOM.Iterable"], "module": "ESNext", "jsx": "react-jsx", "jsxImportSource": "@emotion/react", @@ -9,9 +9,16 @@ "emitDeclarationOnly": true, "rootDir": "src", "outDir": "dist", - "types": [], - "paths": {"@sentry/scraps/*": ["./src/*"]} + "types": ["node"], + "paths": { + "@sentry/scraps/*": ["./src/*"] + } }, "include": ["src"], - "exclude": ["src/**/*.spec.tsx"] + "exclude": ["src/**/*.spec.tsx"], + "references": [ + { + "path": "../icons" + } + ] } diff --git a/static/packages/scraps/tsconfig.test.json b/static/packages/scraps/tsconfig.test.json index 78f6dda319c8..a5dbf741cc98 100644 --- a/static/packages/scraps/tsconfig.test.json +++ b/static/packages/scraps/tsconfig.test.json @@ -9,5 +9,10 @@ "types": ["jest", "node", "@testing-library/jest-dom"] }, "include": ["src", "test"], - "exclude": [] + "exclude": [], + "references": [ + { + "path": "../icons" + } + ] } diff --git a/tests/js/fixtures/theme.tsx b/tests/js/fixtures/theme.tsx index a8950b21acf3..920f5acc8e12 100644 --- a/tests/js/fixtures/theme.tsx +++ b/tests/js/fixtures/theme.tsx @@ -1,4 +1,4 @@ -import {lightTheme} from 'sentry/utils/theme/theme'; +import {lightTheme} from '@sentry/scraps/theme'; export const ThemeFixture = () => { return lightTheme; diff --git a/tests/js/sentry-test/snapshots/snapshot-framework.ts b/tests/js/sentry-test/snapshots/snapshot-framework.ts index d1956933c4d1..dee5f09393c6 100644 --- a/tests/js/sentry-test/snapshots/snapshot-framework.ts +++ b/tests/js/sentry-test/snapshots/snapshot-framework.ts @@ -2,7 +2,7 @@ import type {ReactElement} from 'react'; import {Tooltip as mockTooltip} from 'sentry-test/snapshots/mocks/tooltip'; -import {lightTheme} from 'sentry/utils/theme/theme'; +import {lightTheme} from '@sentry/scraps/theme'; import {closeBrowser, takeSnapshot, type SnapshotInteraction} from './snapshot'; import type {SnapshotTestMetadata} from './snapshot-image-metadata'; diff --git a/tests/js/sentry-test/snapshots/snapshotThemeProvider.tsx b/tests/js/sentry-test/snapshots/snapshotThemeProvider.tsx index ad665d096f1c..fcfc7f52279d 100644 --- a/tests/js/sentry-test/snapshots/snapshotThemeProvider.tsx +++ b/tests/js/sentry-test/snapshots/snapshotThemeProvider.tsx @@ -1,7 +1,7 @@ import type {ReactElement} from 'react'; import {ThemeProvider} from '@emotion/react'; -import {darkTheme, lightTheme} from 'sentry/utils/theme/theme'; +import {darkTheme, lightTheme} from '@sentry/scraps/theme'; import type {SnapshotTheme} from './snapshotScenarios';