mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(core)!: tighten constrained jsx boundary
This commit is contained in:
@@ -66,14 +66,14 @@ describe('compile (transformImports — bare-string rule)', () => {
|
||||
|
||||
describe('compile (transformImports — function rule)', () => {
|
||||
it('rewrites per-identifier source and bucket-merges by resolved target', async () => {
|
||||
const source = `import { Alpha, Beta } from '@fixture/components';\nconst _ = [Alpha, Beta];`;
|
||||
const source = `import { Alpha, Beta } from '@fixture/widgets';\nconst _ = [Alpha, Beta];`;
|
||||
const { code } = await compileJsx(source, {
|
||||
imports: {
|
||||
'@fixture/components': (name) => ({ source: `./ui/${name.toLowerCase()}`, name }),
|
||||
'@fixture/widgets': (name) => ({ source: `./widgets/${name.toLowerCase()}`, name }),
|
||||
},
|
||||
});
|
||||
expect(code).toContain(`import { Alpha } from "./ui/alpha"`);
|
||||
expect(code).toContain(`import { Beta } from "./ui/beta"`);
|
||||
expect(code).toContain(`import { Alpha } from "./widgets/alpha"`);
|
||||
expect(code).toContain(`import { Beta } from "./widgets/beta"`);
|
||||
});
|
||||
|
||||
it('renames identifiers when the rule returns a different `name`', async () => {
|
||||
@@ -154,7 +154,7 @@ describe('childAsProp', () => {
|
||||
|
||||
describe('replaceJsxChild', () => {
|
||||
it('replaces matched JSX children with expression helpers', async () => {
|
||||
const source = `function App({ values }){ return <Container><Token name="poster-image"/></Container>; }`;
|
||||
const source = `function App({ values }){ return <Host><Token name="poster-image"/></Host>; }`;
|
||||
const { code } = await compileJsx(source, {
|
||||
transforms: [
|
||||
replaceJsxChild({
|
||||
@@ -164,68 +164,64 @@ describe('replaceJsxChild', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(collapse(code)).toContain(collapse(`<Container>{values["poster-image"]}</Container>`));
|
||||
expect(collapse(code)).toContain(collapse(`<Host>{values["poster-image"]}</Host>`));
|
||||
});
|
||||
});
|
||||
|
||||
describe('addProp', () => {
|
||||
it('emits a JSX value by default and adds the import', async () => {
|
||||
const source = `function App(){ return <PlayButton/>; }`;
|
||||
const source = `function App(){ return <Action/>; }`;
|
||||
const { code } = await compileJsx(source, {
|
||||
transforms: [
|
||||
addProp({ match: byTag('PlayButton'), prop: 'render', value: { source: './button', name: 'Button' } }),
|
||||
],
|
||||
transforms: [addProp({ match: byTag('Action'), prop: 'render', value: { source: './button', name: 'Button' } })],
|
||||
});
|
||||
expect(collapse(code)).toContain(collapse(`<PlayButton render={<Button/>}/>`));
|
||||
expect(collapse(code)).toContain(collapse(`<Action render={<Button/>}/>`));
|
||||
expect(code).toContain(`import { Button } from "./button"`);
|
||||
});
|
||||
|
||||
it('emits a bare reference when kind is "ref"', async () => {
|
||||
const source = `function App(){ return <PlayButton/>; }`;
|
||||
const source = `function App(){ return <Action/>; }`;
|
||||
const { code } = await compileJsx(source, {
|
||||
transforms: [
|
||||
addProp({
|
||||
match: byTag('PlayButton'),
|
||||
match: byTag('Action'),
|
||||
prop: 'as',
|
||||
value: { source: './button', name: 'Button', kind: 'ref' },
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(collapse(code)).toContain(collapse(`<PlayButton as={Button}/>`));
|
||||
expect(collapse(code)).toContain(collapse(`<Action as={Button}/>`));
|
||||
});
|
||||
|
||||
it('skips elements where the prop is already set', async () => {
|
||||
const source = `function App(){ return <PlayButton render={<X/>}/>; }`;
|
||||
const source = `function App(){ return <Action render={<X/>}/>; }`;
|
||||
const { code } = await compileJsx(source, {
|
||||
transforms: [
|
||||
addProp({ match: byTag('PlayButton'), prop: 'render', value: { source: './button', name: 'Button' } }),
|
||||
],
|
||||
transforms: [addProp({ match: byTag('Action'), prop: 'render', value: { source: './button', name: 'Button' } })],
|
||||
});
|
||||
expect(collapse(code)).toContain(collapse(`<X/>`));
|
||||
expect(code).not.toContain('import { Button }');
|
||||
});
|
||||
|
||||
it('overwrites the existing prop when overwrite is true', async () => {
|
||||
const source = `function App(){ return <PlayButton render={<X/>}/>; }`;
|
||||
const source = `function App(){ return <Action render={<X/>}/>; }`;
|
||||
const { code } = await compileJsx(source, {
|
||||
transforms: [
|
||||
addProp({
|
||||
match: byTag('PlayButton'),
|
||||
match: byTag('Action'),
|
||||
prop: 'render',
|
||||
overwrite: true,
|
||||
value: { source: './button', name: 'Button' },
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(collapse(code)).toContain(collapse(`<PlayButton render={<Button/>}/>`));
|
||||
expect(collapse(code)).toContain(collapse(`<Action render={<Button/>}/>`));
|
||||
});
|
||||
});
|
||||
|
||||
describe('matchers', () => {
|
||||
it('byTag supports dotted tags', async () => {
|
||||
const source = `function App(){ return <Popover.Root foo="bar"/>; }`;
|
||||
const source = `function App(){ return <Alpha.Root foo="bar"/>; }`;
|
||||
const { code } = await compileJsx(source, {
|
||||
transforms: [replace({ match: byTag('Popover.Root'), with: { source: 'pkg', name: 'NewRoot' } })],
|
||||
transforms: [replace({ match: byTag('Alpha.Root'), with: { source: 'pkg', name: 'NewRoot' } })],
|
||||
});
|
||||
expect(code).toContain(`<NewRoot`);
|
||||
});
|
||||
|
||||
-233
@@ -1,233 +0,0 @@
|
||||
import {
|
||||
BufferingIndicator,
|
||||
CaptionsButton,
|
||||
CastButton,
|
||||
Container,
|
||||
Controls,
|
||||
ErrorDialog,
|
||||
FullscreenButton,
|
||||
Gesture,
|
||||
Hotkey,
|
||||
MuteButton,
|
||||
PiPButton,
|
||||
PlayButton,
|
||||
PlaybackRateButton,
|
||||
Popover,
|
||||
Poster,
|
||||
SeekButton,
|
||||
Slider,
|
||||
Time,
|
||||
TimeSlider,
|
||||
Tooltip,
|
||||
VolumeSlider,
|
||||
} from '@fixture/components';
|
||||
import {
|
||||
CaptionsOffIcon,
|
||||
CaptionsOnIcon,
|
||||
CastEnterIcon,
|
||||
CastExitIcon,
|
||||
FullscreenEnterIcon,
|
||||
FullscreenExitIcon,
|
||||
PauseIcon,
|
||||
PipEnterIcon,
|
||||
PipExitIcon,
|
||||
PlayIcon,
|
||||
RestartIcon,
|
||||
SeekIcon,
|
||||
SpinnerIcon,
|
||||
VolumeHighIcon,
|
||||
VolumeLowIcon,
|
||||
VolumeOffIcon,
|
||||
} from '@fixture/icons/components';
|
||||
import { cn } from '@videojs/utils/style';
|
||||
import { video as styles } from '../tailwind';
|
||||
|
||||
const SEEK_TIME = 10;
|
||||
|
||||
const iconButton = cn(styles.button.base, styles.button.subtle, styles.button.icon);
|
||||
|
||||
export interface VideoSkinProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function VideoSkin({ className }: VideoSkinProps) {
|
||||
return (
|
||||
<Container data-skin="default-video" className={cn(styles.container, className)}>
|
||||
<Poster className={styles.poster} />
|
||||
|
||||
<BufferingIndicator className={styles.bufferingIndicator.root}>
|
||||
<div className={styles.bufferingIndicator.container}>
|
||||
<SpinnerIcon className={styles.icon} />
|
||||
</div>
|
||||
</BufferingIndicator>
|
||||
|
||||
<ErrorDialog.Root>
|
||||
<ErrorDialog.Popup className={styles.error.root}>
|
||||
<div className={styles.error.dialog}>
|
||||
<div className={styles.error.content}>
|
||||
<ErrorDialog.Title className={styles.error.title}>Something went wrong.</ErrorDialog.Title>
|
||||
<ErrorDialog.Description className={styles.error.description} />
|
||||
</div>
|
||||
<div className={styles.error.actions}>
|
||||
<ErrorDialog.Close className={cn(styles.button.base, styles.button.primary)}>OK</ErrorDialog.Close>
|
||||
</div>
|
||||
</div>
|
||||
</ErrorDialog.Popup>
|
||||
</ErrorDialog.Root>
|
||||
|
||||
<Controls.Root className={styles.controls}>
|
||||
<Tooltip.Provider>
|
||||
<div className={styles.buttonGroupStart}>
|
||||
<Tooltip.Root side="top">
|
||||
<Tooltip.Trigger>
|
||||
<PlayButton className={cn(iconButton, 'group')}>
|
||||
<RestartIcon className={cn(styles.icon, 'hidden group-data-ended:block')} />
|
||||
<PlayIcon className={cn(styles.icon, 'hidden group-not-data-ended:group-data-paused:block')} />
|
||||
<PauseIcon className={cn(styles.icon, 'hidden group-not-data-paused:group-not-data-ended:block')} />
|
||||
</PlayButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Popup className={styles.popup.tooltip} />
|
||||
</Tooltip.Root>
|
||||
|
||||
<Tooltip.Root side="top">
|
||||
<Tooltip.Trigger>
|
||||
<SeekButton seconds={-SEEK_TIME} className={iconButton}>
|
||||
<span className={styles.iconContainer}>
|
||||
<SeekIcon className={cn(styles.icon, styles.iconFlipped)} />
|
||||
<span className={styles.seek.labelBackward}>{SEEK_TIME}</span>
|
||||
</span>
|
||||
</SeekButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Popup className={styles.popup.tooltip}>Seek backward {SEEK_TIME} seconds</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
|
||||
<Tooltip.Root side="top">
|
||||
<Tooltip.Trigger>
|
||||
<SeekButton seconds={SEEK_TIME} className={iconButton}>
|
||||
<span className={styles.iconContainer}>
|
||||
<SeekIcon className={styles.icon} />
|
||||
<span className={styles.seek.labelForward}>{SEEK_TIME}</span>
|
||||
</span>
|
||||
</SeekButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Popup className={styles.popup.tooltip}>Seek forward {SEEK_TIME} seconds</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
|
||||
<div className={styles.time.group}>
|
||||
<Time.Value type="current" className={styles.time.current} />
|
||||
<TimeSlider.Root className={styles.slider.root}>
|
||||
<TimeSlider.Track className={styles.slider.track}>
|
||||
<TimeSlider.Fill className={cn(styles.slider.fill.base, styles.slider.fill.fill)} />
|
||||
<TimeSlider.Buffer className={cn(styles.slider.fill.base, styles.slider.fill.buffer)} />
|
||||
</TimeSlider.Track>
|
||||
<TimeSlider.Thumb className={cn(styles.slider.thumb.base, styles.slider.thumb.interactive)} />
|
||||
|
||||
<div className={styles.preview.root}>
|
||||
<Slider.Thumbnail className={styles.preview.thumbnail} />
|
||||
<TimeSlider.Value className={styles.preview.time} />
|
||||
<SpinnerIcon className={cn(styles.icon, styles.preview.spinner)} />
|
||||
</div>
|
||||
</TimeSlider.Root>
|
||||
<Time.Value type="duration" className={styles.time.duration} />
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonGroupEnd}>
|
||||
<Tooltip.Root side="top">
|
||||
<Tooltip.Trigger>
|
||||
<PlaybackRateButton className={cn(iconButton, styles.playbackRate.button)} />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Popup className={styles.popup.tooltip}>Toggle playback rate</Tooltip.Popup>
|
||||
</Tooltip.Root>
|
||||
|
||||
<Popover.Root openOnHover delay={200} closeDelay={100} side="top">
|
||||
<Popover.Trigger>
|
||||
<MuteButton className={cn(iconButton, 'group')}>
|
||||
<VolumeOffIcon className={cn(styles.icon, 'hidden group-data-[volume-level=off]:block')} />
|
||||
<VolumeLowIcon className={cn(styles.icon, 'hidden group-data-[volume-level=low]:block')} />
|
||||
<VolumeHighIcon className={cn(styles.icon, 'hidden group-data-[volume-level=high]:block')} />
|
||||
</MuteButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Popup className={cn(styles.popup.popover, styles.popup.volume)}>
|
||||
<VolumeSlider.Root className={styles.slider.root} orientation="vertical" thumbAlignment="edge">
|
||||
<VolumeSlider.Track className={styles.slider.track}>
|
||||
<VolumeSlider.Fill className={cn(styles.slider.fill.base, styles.slider.fill.fill)} />
|
||||
</VolumeSlider.Track>
|
||||
<VolumeSlider.Thumb className={cn(styles.slider.thumb.base, styles.slider.thumb.persistent)} />
|
||||
</VolumeSlider.Root>
|
||||
</Popover.Popup>
|
||||
</Popover.Root>
|
||||
|
||||
<Tooltip.Root side="top">
|
||||
<Tooltip.Trigger>
|
||||
<CaptionsButton className={cn(iconButton, 'group')}>
|
||||
<CaptionsOffIcon className={cn(styles.icon, 'hidden group-not-data-active:block')} />
|
||||
<CaptionsOnIcon className={cn(styles.icon, 'hidden group-data-active:block')} />
|
||||
</CaptionsButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Popup className={styles.popup.tooltip} />
|
||||
</Tooltip.Root>
|
||||
|
||||
<Tooltip.Root side="top">
|
||||
<Tooltip.Trigger>
|
||||
<CastButton className={cn(iconButton, 'group')}>
|
||||
<CastEnterIcon className={cn(styles.icon, 'hidden group-not-data-[cast-state=connected]:block')} />
|
||||
<CastExitIcon className={cn(styles.icon, 'hidden group-data-[cast-state=connected]:block')} />
|
||||
</CastButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Popup className={styles.popup.tooltip} />
|
||||
</Tooltip.Root>
|
||||
|
||||
<Tooltip.Root side="top">
|
||||
<Tooltip.Trigger>
|
||||
<PiPButton className={cn(iconButton, 'group')}>
|
||||
<PipEnterIcon className={cn(styles.icon, 'hidden group-not-data-pip:block')} />
|
||||
<PipExitIcon className={cn(styles.icon, 'hidden group-data-pip:block')} />
|
||||
</PiPButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Popup className={styles.popup.tooltip} />
|
||||
</Tooltip.Root>
|
||||
|
||||
<Tooltip.Root side="top">
|
||||
<Tooltip.Trigger>
|
||||
<FullscreenButton className={cn(iconButton, 'group')}>
|
||||
<FullscreenEnterIcon className={cn(styles.icon, 'hidden group-not-data-fullscreen:block')} />
|
||||
<FullscreenExitIcon className={cn(styles.icon, 'hidden group-data-fullscreen:block')} />
|
||||
</FullscreenButton>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Popup className={styles.popup.tooltip} />
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
</Controls.Root>
|
||||
|
||||
<div className={styles.overlay} />
|
||||
|
||||
{/* Hotkeys */}
|
||||
<Hotkey keys="Space" action="togglePaused" />
|
||||
<Hotkey keys="k" action="togglePaused" />
|
||||
<Hotkey keys="m" action="toggleMuted" />
|
||||
<Hotkey keys="f" action="toggleFullscreen" />
|
||||
<Hotkey keys="c" action="toggleSubtitles" />
|
||||
<Hotkey keys="i" action="togglePictureInPicture" />
|
||||
<Hotkey keys="ArrowRight" action="seekStep" value={5} />
|
||||
<Hotkey keys="ArrowLeft" action="seekStep" value={-5} />
|
||||
<Hotkey keys="l" action="seekStep" value={10} />
|
||||
<Hotkey keys="j" action="seekStep" value={-10} />
|
||||
<Hotkey keys="ArrowUp" action="volumeStep" value={0.05} />
|
||||
<Hotkey keys="ArrowDown" action="volumeStep" value={-0.05} />
|
||||
<Hotkey keys="0-9" action="seekToPercent" />
|
||||
<Hotkey keys="Home" action="seekToPercent" value={0} />
|
||||
<Hotkey keys="End" action="seekToPercent" value={100} />
|
||||
<Hotkey keys=">" action="speedUp" />
|
||||
<Hotkey keys="<" action="speedDown" />
|
||||
|
||||
{/* Gestures */}
|
||||
<Gesture type="tap" action="togglePaused" pointer="mouse" region="center" />
|
||||
<Gesture type="tap" action="toggleControls" pointer="touch" />
|
||||
<Gesture type="doubletap" action="seekStep" value={-10} region="left" />
|
||||
<Gesture type="doubletap" action="toggleFullscreen" region="center" />
|
||||
<Gesture type="doubletap" action="seekStep" value={10} region="right" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,46 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { compile } from '..';
|
||||
import { anyTag, byTag, childAsProp, hasChild, jsx, replace } from '../jsx';
|
||||
import type { ImportRule } from '../transforms';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const skinSource = resolve(__dirname, 'fixtures/video-skin.tsx');
|
||||
|
||||
/**
|
||||
* End-to-end smoke test: feed a representative constrained-JSX video skin
|
||||
* (vendored under `fixtures/`) through `compile()` with the same shape
|
||||
* a package build hook uses, and sanity-check the output's structural
|
||||
* shape. Snapshot-style assertions intentionally use `.toContain` over a full
|
||||
* snapshot to keep the test resilient to incidental whitespace differences
|
||||
* from the TS printer.
|
||||
* End-to-end smoke test for the generic JSX transform pipeline. The compiler
|
||||
* package deliberately avoids Video.js UI semantics here: transforms operate on
|
||||
* JSX tags and imports only.
|
||||
*/
|
||||
describe('integration: default/video skin → JSX', () => {
|
||||
const source = readFileSync(skinSource, 'utf8');
|
||||
describe('integration: JSX transform pipeline', () => {
|
||||
const source = `
|
||||
import { Alpha, Beta, Gamma } from '@fixture/widgets';
|
||||
import { Icon } from '@fixture/icons/components';
|
||||
import { tokens } from '../tokens';
|
||||
|
||||
export function Example() {
|
||||
return (
|
||||
<Alpha.Root className={tokens.root}>
|
||||
<Alpha.Trigger>
|
||||
<Beta className={tokens.action}>
|
||||
<Icon className={tokens.icon} />
|
||||
</Beta>
|
||||
</Alpha.Trigger>
|
||||
<Gamma.Root>
|
||||
<Gamma.Trigger>
|
||||
<Beta className={tokens.secondary} />
|
||||
</Gamma.Trigger>
|
||||
<Gamma.Panel className={tokens.panel} />
|
||||
</Gamma.Root>
|
||||
</Alpha.Root>
|
||||
);
|
||||
}
|
||||
`;
|
||||
let code = '';
|
||||
|
||||
const imports: Record<string, ImportRule> = {
|
||||
'@fixture/components': (name) => ({
|
||||
source: `./src/ui/${name.replace(/^[A-Z]/, (m) => m.toLowerCase()).replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`,
|
||||
'@fixture/widgets': (name) => ({
|
||||
source: `./widgets/${name.replace(/^[A-Z]/, (m) => m.toLowerCase()).replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)}`,
|
||||
name,
|
||||
}),
|
||||
'@fixture/icons/components': '@fixture/icons/jsx',
|
||||
'../tailwind': '@videojs/skins/default/tailwind',
|
||||
'../tokens': '@fixture/tokens',
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -37,13 +50,13 @@ describe('integration: default/video skin → JSX', () => {
|
||||
imports,
|
||||
transforms: [
|
||||
replace({
|
||||
match: byTag('Popover.Root', {
|
||||
when: hasChild(byTag('Popover.Trigger', { when: hasChild(byTag('MuteButton')) })),
|
||||
match: byTag('Gamma.Root', {
|
||||
when: hasChild(byTag('Gamma.Trigger', { when: hasChild(byTag('Beta')) })),
|
||||
}),
|
||||
with: { source: './volume-popover', name: 'VolumePopover' },
|
||||
with: { source: './replacement', name: 'Replacement' },
|
||||
mapChildren: () => [],
|
||||
}),
|
||||
childAsProp({ match: anyTag(['Tooltip.Trigger', 'Popover.Trigger']), prop: 'render' }),
|
||||
childAsProp({ match: anyTag(['Alpha.Trigger', 'Gamma.Trigger']), prop: 'render' }),
|
||||
],
|
||||
}),
|
||||
},
|
||||
@@ -51,12 +64,11 @@ describe('integration: default/video skin → JSX', () => {
|
||||
code = result.code;
|
||||
});
|
||||
|
||||
it('rewrites component imports to per-identifier UI sources', () => {
|
||||
expect(code).toMatch(/import \{ PlayButton \} from "\.\/src\/ui\/play-button"/);
|
||||
// MuteButton lives under the volume Popover.Root subtree, which is replaced
|
||||
// wholesale by VolumePopover — its import is correctly dropped by the
|
||||
// unused-imports cleanup pass.
|
||||
expect(code).not.toMatch(/import \{ MuteButton \}/);
|
||||
it('rewrites component imports to per-identifier sources', () => {
|
||||
expect(code).toMatch(/import \{ Alpha \} from "\.\/widgets\/alpha"/);
|
||||
expect(code).toMatch(/import \{ Beta \} from "\.\/widgets\/beta"/);
|
||||
// Gamma only appears under the replaced subtree, so cleanup drops it.
|
||||
expect(code).not.toMatch(/import \{ Gamma \}/);
|
||||
});
|
||||
|
||||
it('rewrites icon component imports', () => {
|
||||
@@ -64,16 +76,16 @@ describe('integration: default/video skin → JSX', () => {
|
||||
expect(code).not.toContain('@fixture/icons/components');
|
||||
});
|
||||
|
||||
it('substitutes the volume Popover.Root with VolumePopover', () => {
|
||||
expect(code).toContain('<VolumePopover');
|
||||
expect(code).toContain('import { VolumePopover }');
|
||||
it('substitutes the matched subtree with the replacement component', () => {
|
||||
expect(code).toContain('<Replacement');
|
||||
expect(code).toContain('import { Replacement }');
|
||||
});
|
||||
|
||||
it('lifts Tooltip.Trigger / Popover.Trigger children into render props', () => {
|
||||
expect(code).toMatch(/<Tooltip\.Trigger render=\{<PlayButton/);
|
||||
it('lifts matched trigger children into render props', () => {
|
||||
expect(code).toMatch(/<Alpha\.Trigger render=\{<Beta/);
|
||||
});
|
||||
|
||||
it('keeps unrelated relative tailwind import re-routed to the skins package', () => {
|
||||
expect(code).toContain('@videojs/skins/default/tailwind');
|
||||
it('keeps unrelated relative token imports re-routed by config', () => {
|
||||
expect(code).toContain('@fixture/tokens');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user