/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { MobilePopup } from '../MobilePopup';
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock('../MobilePopup.style', () => ({
useMobileActionDrawerStyle: () => ({
componentCls: 'nb-mobile-action-drawer',
hashId: 'hash',
}),
}));
vi.mock('../../lazy-helper', () => ({
lazy: (_loader: unknown, name: string) => {
if (name === 'Popup') {
const Popup = ({
children,
bodyStyle,
maskStyle,
style,
}: {
children: React.ReactNode;
bodyStyle?: React.CSSProperties;
maskStyle?: React.CSSProperties;
style?: React.CSSProperties;
}) => (
);
return { Popup };
}
return {
CloseOutline: () => ,
};
},
}));
describe('MobilePopup', () => {
const MobilePopupWithDrawerStyles = MobilePopup as React.ComponentType<
React.ComponentProps & { styles?: { body?: React.CSSProperties } }
>;
afterEach(() => {
vi.unstubAllGlobals();
});
it('constrains the default popup to the dynamic viewport and keeps the body scrollable', () => {
vi.stubGlobal('CSS', {
supports: vi.fn((property: string, value: string) => property === 'height' && value === '100dvh'),
});
render(
body
,
);
const maxHeight = 'calc(100dvh - var(--nb-mobile-page-header-height, 46px))';
expect(screen.getByTestId('mobile-popup')).toHaveStyle({ maxHeight });
expect(screen.getByTestId('mobile-popup-body')).toHaveStyle({
maxHeight,
overflowY: 'auto',
overflowX: 'hidden',
});
});
it('falls back to the layout viewport when dynamic viewport units are unavailable', () => {
vi.stubGlobal('CSS', {
supports: vi.fn(() => false),
});
render(
body
,
);
expect(screen.getByTestId('mobile-popup-body')).toHaveStyle({
maxHeight: 'calc(100vh - var(--nb-mobile-page-header-height, 46px))',
});
});
it('applies drawer body styles as max bounds without forcing fixed half-window height', () => {
render(
body
,
);
expect(screen.getByTestId('mobile-popup')).toHaveStyle({
maxHeight: '50vh',
});
expect(screen.getByTestId('mobile-popup-body')).toHaveStyle({
maxHeight: '50vh',
});
expect(screen.getByTestId('mobile-popup-mask')).not.toHaveStyle({
maxHeight: '50vh',
});
});
it('closes from the header close icon with Enter or Space', () => {
const onClose = vi.fn();
render(
body
,
);
const closeButton = screen.getByRole('button', { name: 'Close' });
fireEvent.keyDown(closeButton, { key: 'Enter' });
fireEvent.keyDown(closeButton, { key: ' ' });
expect(onClose).toHaveBeenCalledTimes(2);
});
it('keeps long header titles in a constrained title element separate from the close button', () => {
const longTitle = 'A very long table column title that should not push the close button outside the drawer';
render(
body
,
);
expect(screen.getByText(longTitle)).toHaveClass('nb-mobile-action-drawer-title');
expect(screen.getByRole('button', { name: 'Close' })).toHaveClass('nb-mobile-action-drawer-close-icon');
});
});