Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
77ad148
feat: first POC of the usage of pragmatic dnd instead of dnd kit
marcosmoura Apr 8, 2024
3f56d45
fix: remove unused imports and make the component a little more stable
marcosmoura Apr 23, 2024
82d112e
Merge branch 'main' into experimental/react-draggable-dialog/use-prag…
marcosmoura Sep 8, 2025
3b47184
fix(react-draggable-dialog): improve calculation performance after mo…
marcosmoura Sep 9, 2025
a000a7b
Merge branch 'main' into fix/react-draggable-dialog/improve-performan…
marcosmoura Sep 9, 2025
2cb38b0
Change files
marcosmoura Sep 9, 2025
2d4207f
fix: update changelog message
marcosmoura Sep 9, 2025
c2347ff
fix: dedupe
marcosmoura Sep 9, 2025
f7acb6a
fix: dedupe
marcosmoura Sep 9, 2025
183e134
fix: dedupe
marcosmoura Sep 9, 2025
c81b0b2
fix: dedupe
marcosmoura Sep 9, 2025
e33280c
test(react-draggable-dialog): add a lot more test cases for better co…
marcosmoura Sep 9, 2025
0e08bcb
fix: clear animation frame on cleanup
marcosmoura Sep 11, 2025
e9f195c
fix: remove unnecessary useMemo
marcosmoura Sep 11, 2025
96059fa
fix: remove unnecessary useMemo
marcosmoura Sep 11, 2025
de1845c
Merge branch 'main' into fix/react-draggable-dialog/improve-performan…
marcosmoura Sep 19, 2025
8019fad
fix: revert changes to yarn.lock
marcosmoura Oct 14, 2025
cf9eb3c
fix: upgrade dnd-kit
marcosmoura Oct 14, 2025
716f2b6
Merge branch 'main' into fix/react-draggable-dialog/improve-performan…
marcosmoura Oct 14, 2025
be6f79f
fix: formatting
marcosmoura Oct 14, 2025
9f1158e
fix: remove eslint comment
marcosmoura Oct 14, 2025
309edb6
fix: update swc config for tests
marcosmoura Oct 15, 2025
d11c1a3
Merge branch 'fix/react-draggable-dialog/improve-performance-after-mo…
marcosmoura Oct 15, 2025
cb3a6cf
Merge branch 'main' into test/react-draggable-dialog/add-more-complet…
marcosmoura Oct 15, 2025
e1a1562
Change files
marcosmoura Oct 15, 2025
03ecfa4
test: improve readability of test cases
marcosmoura Oct 22, 2025
88bcc0e
Merge branch 'main' into test/react-draggable-dialog/add-more-complet…
marcosmoura Oct 22, 2025
0792182
Merge branch 'main' into test/react-draggable-dialog/add-more-complet…
marcosmoura Oct 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "fix: add comprehensive number of tests",
"packageName": "@fluentui-contrib/react-draggable-dialog",
"email": "[email protected]",
"dependentChangeType": "patch"
}
7 changes: 5 additions & 2 deletions packages/react-draggable-dialog/jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable */
import { readFileSync } from 'fs';
import { Config } from 'jest';

// Reading the SWC compilation config and remove the "exclude"
// for the test files to be compiled by SWC
Expand All @@ -14,11 +15,11 @@ if (swcJestConfig.swcrc === undefined) {
}

// Uncomment if using global setup/teardown files being transformed via swc
// https://nx.dev/nx-api/jest/documents/overview#global-setupteardown-with-nx-libraries
// https://nx.dev/docs/technologies/test-tools/jest/introduction#global-setupteardown-with-nx-libraries
// jest needs EsModule Interop to find the default exported setup/teardown functions
// swcJestConfig.module.noInterop = false;

export default {
const config: Config = {
displayName: 'react-draggable-dialog',
preset: '../../jest.preset.js',
transform: {
Expand All @@ -28,3 +29,5 @@ export default {
testEnvironment: 'jsdom',
coverageDirectory: '../../coverage/packages/react-draggable-dialog',
};

export default config;
4 changes: 2 additions & 2 deletions packages/react-draggable-dialog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"dependencies": {
"@swc/helpers": "~0.5.11",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/utilities": "^3.2.2",
"@dnd-kit/modifiers": "^9.0.0"
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/utilities": "^3.2.2"
},
"peerDependencies": {
"@fluentui/react-components": ">=9.70.0 <10.0.0",
Expand Down
10 changes: 10 additions & 0 deletions packages/react-draggable-dialog/playwright/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
// Import styles, initialize component theme here.
import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import { FluentProvider, webLightTheme } from '@fluentui/react-components';

beforeMount(async ({ App }) => {
return (
<FluentProvider theme={webLightTheme}>
<App />
</FluentProvider>
);
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,67 @@
import * as React from 'react';
import { test, expect } from '@playwright/experimental-ct-react';

import { DraggableDialogTestExample } from './DraggableDialogExample.component-browser-spec';
import { Locator } from 'playwright/test';

test.use({ viewport: { width: 500, height: 500 } });

test('should render basic component', async ({ mount }) => {
const component = await mount(<div>Example</div>);
const evaluate = async (locator: Locator) => {
return locator.evaluate((el: HTMLElement) => ({
top: el.style.top,
left: el.style.left,
}));
};

test.describe('DraggableDialog (Playwright)', () => {
test('does not render when open is false', async ({ mount, page }) => {
await mount(<DraggableDialogTestExample open={false} />);

await expect(page.getByRole('dialog')).toHaveCount(0);
});

test('renders when open is true', async ({ mount, page }) => {
await mount(<DraggableDialogTestExample open />);

const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible();
await expect(page.getByTestId('content')).toHaveText('Dialog Content');
});

test('respects controlled position prop', async ({ mount, page }) => {
await mount(
<DraggableDialogTestExample open position={{ x: 40, y: 60 }} />
);
const surface = page.locator('.fui-DraggableDialogSurface');

await expect(surface).toBeVisible();

const inlinePos = await evaluate(surface);

expect(inlinePos.top).toBe('60px');
expect(inlinePos.left).toBe('40px');
});

test('applies margin with viewport boundary to initial positioning', async ({
mount,
page,
}) => {
const margin = 50;

await mount(
<DraggableDialogTestExample
open
margin={margin}
contentText="Viewport Boundary"
/>
);

const surface = page.locator('.fui-DraggableDialogSurface');
const styles = await evaluate(surface);
const topNum = parseInt(styles.top, 10);
const leftNum = parseInt(styles.left, 10);

await expect(component).toBeVisible();
expect(topNum).toBeGreaterThanOrEqual(margin);
expect(leftNum).toBeGreaterThanOrEqual(margin);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,34 @@ import { DraggableDialog } from './DraggableDialog';
import { DraggableDialogSurface } from '../DraggableDialogSurface';

describe('DraggableDialog', () => {
it('should render', () => {
// useAnimationFrame inside the component schedules callbacks; we need fake timers to flush them
beforeAll(() => {
jest.useFakeTimers();
});

afterAll(() => {
jest.useRealTimers();
});
const TestContent = ({ text = 'Test Content' }: { text?: string }) => (
<DraggableDialogSurface>
<div>{text}</div>
</DraggableDialogSurface>
);

it('should not render if open is false', () => {
const text = 'Context';
const { queryByText } = render(
<DraggableDialog open={false}>
<DraggableDialogSurface>
<div>{text}</div>
</DraggableDialogSurface>
</DraggableDialog>
);

expect(queryByText(text)).toBeNull();
});

it('should render if open is true', () => {
const text = 'Context';
const { getByText } = render(
<DraggableDialog open>
Expand All @@ -17,4 +44,189 @@ describe('DraggableDialog', () => {

expect(() => getByText(text)).toBeTruthy();
});

describe('Props', () => {
it('should accept and pass through all Dialog props', () => {
const modalType = 'alert';
const onOpenChange = jest.fn();
const { getByRole } = render(
<DraggableDialog
open
onOpenChange={onOpenChange}
inertTrapFocus={true}
modalType={modalType}
>
<TestContent />
</DraggableDialog>
);

expect(getByRole('alertdialog')).toBeDefined();
});

it('should handle boundary prop as viewport (default)', () => {
const { getByText } = render(
<DraggableDialog open boundary="viewport">
<TestContent />
</DraggableDialog>
);

expect(getByText('Test Content')).toBeDefined();
});

it('should handle boundary prop as null', () => {
const { getByText } = render(
<DraggableDialog open boundary={null}>
<TestContent />
</DraggableDialog>
);

expect(getByText('Test Content')).toBeDefined();
});

it('should handle boundary prop as ref', () => {
const boundaryRef = React.createRef<HTMLDivElement>();
const { getByText } = render(
<div>
<div ref={boundaryRef} />
<DraggableDialog open boundary={boundaryRef}>
<TestContent />
</DraggableDialog>
</div>
);

const textEl = getByText('Test Content');

expect(textEl).toBeDefined();
expect(boundaryRef.current).toBeDefined();
expect(boundaryRef.current?.contains(textEl)).toBe(true);
});

it('should handle margin prop', () => {
const { getByText } = render(
<DraggableDialog open margin={10}>
<TestContent />
</DraggableDialog>
);

expect(getByText('Test Content')).toBeDefined();
});

it('should handle position prop', () => {
const { getByText } = render(
<DraggableDialog open position={{ x: 100, y: 200 }}>
<TestContent />
</DraggableDialog>
);

expect(getByText('Test Content')).toBeDefined();
});

it('should handle onPositionChange callback', () => {
const onPositionChange = jest.fn();
const { getByText } = render(
<DraggableDialog open onPositionChange={onPositionChange}>
<TestContent />
</DraggableDialog>
);

expect(getByText('Test Content')).toBeDefined();
// The callback should be ready to be called during drag events
// We verify the callback is a function and hasn't been called during initial render
expect(typeof onPositionChange).toBe('function');
expect(onPositionChange).not.toHaveBeenCalled();
});

it('should call onPositionChange during drag interactions', () => {
const onPositionChange = jest.fn();
const { getByRole } = render(
<DraggableDialog open onPositionChange={onPositionChange}>
<TestContent />
</DraggableDialog>
);

// Find the dialog surface element
const draggableElement = getByRole('dialog');

if (!draggableElement) {
throw new Error('Could not find a draggable element in the dialog');
}

// Since dnd-kit requires complex event simulation, we'll test that the component
// properly sets up the drag context and the onPositionChange callback is available.
// The actual drag functionality is tested at the hook level and integration level.

// Verify that the element exists and is properly rendered
expect(draggableElement).toBeDefined();

// The callback should be ready to be called during drag events
expect(typeof onPositionChange).toBe('function');
expect(onPositionChange).not.toHaveBeenCalled();

// Verify the dialog is properly set up for dragging by checking it's a dialog element
expect(draggableElement.getAttribute('role')).toBe('dialog');
});
});

describe('Memoization', () => {
let renderCount = 0;

const TestComponent = React.memo(() => {
renderCount++;
return <TestContent text={`Render count: ${renderCount}`} />;
});

beforeEach(() => {
renderCount = 0;
});

it('should be a memoized component with correct displayName', () => {
expect(DraggableDialog.displayName).toBe('DraggableDialog');
expect(typeof DraggableDialog).toBe('object'); // React.memo returns an object
});

it('should memoize when props do not change', () => {
const { rerender, getByText } = render(
<DraggableDialog open>
<TestComponent />
</DraggableDialog>
);

expect(getByText('Render count: 1')).toBeDefined();

// Rerender with same props - should not re-render child
rerender(
<DraggableDialog open>
<TestComponent />
</DraggableDialog>
);

expect(getByText('Render count: 1')).toBeDefined();
});

it('should re-render when critical props change', () => {
const { rerender, getByText, queryByText } = render(
<DraggableDialog open>
<TestComponent />
</DraggableDialog>
);

expect(getByText('Render count: 1')).toBeDefined();

// Test open prop change
rerender(
<DraggableDialog open={false}>
<TestComponent />
</DraggableDialog>
);
expect(queryByText('Render count: 1')).toBeNull();

// Test children change
rerender(
<DraggableDialog open>
<TestContent text="Updated Content" />
</DraggableDialog>
);
expect(getByText('Updated Content')).toBeDefined();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';

import { DraggableDialog, DraggableDialogProps } from '../DraggableDialog';
import { DraggableDialogSurface } from '../DraggableDialogSurface';

export type DraggableDialogTestExampleProps = Omit<
DraggableDialogProps,
'children'
> & {
children?: React.ReactNode;
contentText?: string;
surfaceProps?: React.HTMLAttributes<HTMLDivElement>;
};

export const DraggableDialogTestExample: React.FC<
DraggableDialogTestExampleProps
> = (props) => {
const {
contentText = 'Dialog Content',
children,
surfaceProps,
...rest
} = props;

return (
<DraggableDialog {...rest}>
<DraggableDialogSurface tabIndex={0} {...surfaceProps}>
<div data-testid="content">{children || contentText}</div>
</DraggableDialogSurface>
</DraggableDialog>
);
};
Loading