Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@
"@playwright/test": "^1.42.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@stream-io/stream-chat-css": "^5.11.2",
"@stream-io/stream-chat-css": "^5.12.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
Expand Down
10 changes: 8 additions & 2 deletions src/components/Attachment/__tests__/Geolocation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@ describe.each([
});

it('renders own live location', async () => {
const location = generateLiveLocationResponse({ user_id: ownUser.id });
const location = generateLiveLocationResponse({
end_at: new Date(Date.now() + 10000).toISOString(),
user_id: ownUser.id,
});
await renderComponent({
props: { GeolocationAttachmentMapPlaceholder, GeolocationMap, location },
});
Expand All @@ -129,7 +132,10 @@ describe.each([
}
});
it("other user's live location", async () => {
const location = generateLiveLocationResponse({ user_id: otherUser.id });
const location = generateLiveLocationResponse({
end_at: new Date(Date.now() + 10000).toISOString(),
user_id: otherUser.id,
});
await renderComponent({
props: { GeolocationAttachmentMapPlaceholder, GeolocationMap, location },
});
Expand Down
3 changes: 3 additions & 0 deletions src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type ChannelPropsForwardedToComponentContext = Pick<
| 'MessageStatus'
| 'MessageSystem'
| 'MessageTimestamp'
| 'Modal'
| 'ModalGallery'
| 'PinIndicator'
| 'PollActions'
Expand Down Expand Up @@ -1221,6 +1222,7 @@ const ChannelInner = (
MessageStatus: props.MessageStatus,
MessageSystem: props.MessageSystem,
MessageTimestamp: props.MessageTimestamp,
Modal: props.Modal,
ModalGallery: props.ModalGallery,
PinIndicator: props.PinIndicator,
PollActions: props.PollActions,
Expand Down Expand Up @@ -1288,6 +1290,7 @@ const ChannelInner = (
props.MessageStatus,
props.MessageSystem,
props.MessageTimestamp,
props.Modal,
props.ModalGallery,
props.PinIndicator,
props.PollActions,
Expand Down
6 changes: 4 additions & 2 deletions src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { useChannelsQueryState } from './hooks/useChannelsQueryState';
import { ChatProvider } from '../../context/ChatContext';
import { TranslationProvider } from '../../context/TranslationContext';
import type { CustomClasses } from '../../context/ChatContext';
import type { MessageContextValue } from '../../context';
import { type MessageContextValue, ModalDialogManagerProvider } from '../../context';
import type { SupportedTranslations } from '../../i18n/types';
import type { Streami18n } from '../../i18n/Streami18n';

Expand Down Expand Up @@ -110,7 +110,9 @@ export const Chat = (props: PropsWithChildren<ChatProps>) => {

return (
<ChatProvider value={chatContextValue}>
<TranslationProvider value={translators}>{children}</TranslationProvider>
<TranslationProvider value={translators}>
<ModalDialogManagerProvider>{children}</ModalDialogManagerProvider>
</TranslationProvider>
</ChatProvider>
);
};
7 changes: 6 additions & 1 deletion src/components/Dialog/DialogManager.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { nanoid } from 'nanoid';
import { StateStore } from 'stream-chat';

export type GetOrCreateDialogParams = {
export type GetDialogParams = {
id: DialogId;
};
export type GetOrCreateDialogParams = GetDialogParams;

type DialogId = string;

Expand Down Expand Up @@ -57,6 +58,10 @@ export class DialogManager {
);
}

get(id: DialogId) {
return this.state.getLatestValue().dialogsById[id];
}

getOrCreate({ id }: GetOrCreateDialogParams) {
let dialog = this.state.getLatestValue().dialogsById[id];
if (!dialog) {
Expand Down
6 changes: 4 additions & 2 deletions src/components/Dialog/DialogPortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export const DialogPortalDestination = () => {
const { dialogManager } = useDialogManager();
const openedDialogCount = useOpenedDialogCount();

if (!openedDialogCount) return null;

return (
<div
className='str-chat__dialog-overlay'
Expand All @@ -31,8 +33,8 @@ export const DialogPortalEntry = ({
children,
dialogId,
}: PropsWithChildren<DialogPortalEntryProps>) => {
const { dialogManager } = useDialogManager();
const dialogIsOpen = useDialogIsOpen(dialogId);
const { dialogManager } = useDialogManager({ dialogId });
const dialogIsOpen = useDialogIsOpen(dialogId, dialogManager.id);

const getPortalDestination = useCallback(
() => document.querySelector(`div[data-str-chat__portal-id="${dialogManager.id}"]`),
Expand Down
257 changes: 257 additions & 0 deletions src/components/Dialog/__tests__/DialogManagerContext.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
import React from 'react';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
DialogManagerProvider,
useDialogManager,
} from '../../../context/DialogManagerContext';

import '@testing-library/jest-dom';
import { useDialogIsOpen, useOpenedDialogCount } from '../hooks';

const TEST_IDS = {
CLOSE_DIALOG: 'close-dialog',
DIALOG_COUNT: 'dialog-count',
DIALOG_OPEN: 'dialog-open',
MANAGER_ID_DISPLAY: 'manager-id-display',
OPEN_DIALOG: 'open-dialog',
TEST_COMPONENT: 'test-component',
};

const TEST_MANAGER_ID = 'test-manager';
const SHARED_MANAGER_ID = 'shared-manager';
const MANAGER_1_ID = 'manager-1';
const MANAGER_2_ID = 'manager-2';

const TestComponent = ({ dialogId, dialogManagerId, testId }) => {
const { dialogManager } = useDialogManager({ dialogId, dialogManagerId });
const openDialogCount = useOpenedDialogCount({ dialogManagerId });
const isOpen = useDialogIsOpen(dialogId, dialogManagerId);
return (
<div data-testid={testId ?? TEST_IDS.TEST_COMPONENT}>
<span data-testid={TEST_IDS.MANAGER_ID_DISPLAY}>{dialogManager?.id}</span>
<span data-testid={TEST_IDS.DIALOG_COUNT}>{openDialogCount}</span>
<span data-testid={TEST_IDS.DIALOG_OPEN}>{isOpen ? 'true' : 'false'}</span>
</div>
);
};

const DialogTestComponent = ({ dialogId, managerId }) => {
const { dialogManager } = useDialogManager({ dialogManagerId: managerId });

const handleOpenDialog = () => {
if (dialogManager) {
dialogManager.open({ id: dialogId });
}
};

const handleCloseDialog = () => {
if (dialogManager) {
dialogManager.close(dialogId);
}
};

return (
<div>
<button data-testid={TEST_IDS.OPEN_DIALOG} onClick={handleOpenDialog}>
Open
</button>
<button data-testid={TEST_IDS.CLOSE_DIALOG} onClick={handleCloseDialog}>
Close
</button>
</div>
);
};

describe('DialogManagerContext', () => {
describe('DialogManagerProvider', () => {
it('does not create a new dialog manager when no id is provided', () => {
render(
<DialogManagerProvider>
<TestComponent />
</DialogManagerProvider>,
);

expect(screen.getByTestId(TEST_IDS.DIALOG_COUNT).textContent).toBe('0');
});

it('creates a new dialog manager and adds it to the manager pool when id is provided', () => {
render(
<DialogManagerProvider id={TEST_MANAGER_ID}>
<TestComponent dialogManagerId={TEST_MANAGER_ID} />
</DialogManagerProvider>,
);

const managerId = screen.getByTestId(TEST_IDS.MANAGER_ID_DISPLAY).textContent;
expect(managerId).toBe(TEST_MANAGER_ID);
expect(screen.getByTestId(TEST_IDS.DIALOG_COUNT).textContent).toBe('0');
});

it('provides dialog manager to non-child components', () => {
render(
<DialogManagerProvider id={MANAGER_1_ID}>
<DialogManagerProvider id={MANAGER_2_ID} />
<TestComponent dialogManagerId={MANAGER_2_ID} />
</DialogManagerProvider>,
);
const managerId = screen.getByTestId(TEST_IDS.MANAGER_ID_DISPLAY).textContent;
expect(managerId).toBe(MANAGER_2_ID);
expect(screen.getByTestId(TEST_IDS.DIALOG_COUNT).textContent).toBe('0');
});

it('removes the dialog manager from the pool upon unmount', () => {
const { rerender } = render(
<DialogManagerProvider id={TEST_MANAGER_ID}>
<TestComponent dialogManagerId={TEST_MANAGER_ID} />
</DialogManagerProvider>,
);

const managerId = screen.getByTestId(TEST_IDS.MANAGER_ID_DISPLAY).textContent;
expect(managerId).toBe(TEST_MANAGER_ID);

rerender(
<DialogManagerProvider id='different-manager'>
<TestComponent dialogManagerId={TEST_MANAGER_ID} />
</DialogManagerProvider>,
);

expect(screen.getByTestId(TEST_IDS.MANAGER_ID_DISPLAY)).toHaveTextContent(
'different-manager',
);
expect(screen.getByTestId(TEST_IDS.DIALOG_COUNT)).toHaveTextContent('0');
});

it('retrieves existing dialog manager and does not create a new dialog manager', () => {
const dialogId = 'shared-dialog';
render(
<DialogManagerProvider id={SHARED_MANAGER_ID}>
<TestComponent
dialogId={dialogId}
dialogManagerId={SHARED_MANAGER_ID}
testId={'component-1'}
/>
<DialogManagerProvider id={SHARED_MANAGER_ID}>
<DialogTestComponent dialogId={dialogId} managerId={SHARED_MANAGER_ID} />
<TestComponent
dialogId={dialogId}
dialogManagerId={SHARED_MANAGER_ID}
testId={'component-2'}
/>
</DialogManagerProvider>
</DialogManagerProvider>,
);

const component1 = screen.getByTestId('component-1');
const component2 = screen.getByTestId('component-2');

expect(
component1.querySelector(`[data-testid="${TEST_IDS.MANAGER_ID_DISPLAY}"`),
).toHaveTextContent(SHARED_MANAGER_ID);
expect(
component2.querySelector(`[data-testid="${TEST_IDS.MANAGER_ID_DISPLAY}"`),
).toHaveTextContent(SHARED_MANAGER_ID);

act(() => {
fireEvent.click(screen.getByTestId(TEST_IDS.OPEN_DIALOG));
});
expect(
component1.querySelector(`[data-testid="${TEST_IDS.DIALOG_COUNT}"`),
).toHaveTextContent('1');
expect(
component2.querySelector(`[data-testid="${TEST_IDS.DIALOG_COUNT}"`),
).toHaveTextContent('1');
expect(
component1.querySelector(`[data-testid="${TEST_IDS.DIALOG_OPEN}"`),
).toHaveTextContent('true');
expect(
component2.querySelector(`[data-testid="${TEST_IDS.DIALOG_OPEN}"`),
).toHaveTextContent('true');
});

it('creates different managers for different IDs', () => {
render(
<DialogManagerProvider id={MANAGER_1_ID}>
<DialogManagerProvider id={MANAGER_2_ID}>
<DialogTestComponent dialogId='dialog-1' managerId={MANAGER_1_ID} />
<DialogTestComponent dialogId='dialog-2' managerId={MANAGER_2_ID} />
<TestComponent dialogManagerId={MANAGER_1_ID} />
<TestComponent dialogManagerId={MANAGER_2_ID} />
</DialogManagerProvider>
</DialogManagerProvider>,
);

const testComponents = screen.getAllByTestId(TEST_IDS.TEST_COMPONENT);
expect(testComponents).toHaveLength(2);

const manager1Id = testComponents[0].querySelector(
`[data-testid="${TEST_IDS.MANAGER_ID_DISPLAY}"]`,
).textContent;
const manager2Id = testComponents[1].querySelector(
`[data-testid="${TEST_IDS.MANAGER_ID_DISPLAY}"]`,
).textContent;

expect(manager1Id).toBe(MANAGER_1_ID);
expect(manager2Id).toBe(MANAGER_2_ID);

act(() => {
screen.getAllByTestId(TEST_IDS.OPEN_DIALOG)[0].click();
});

const manager1Count = testComponents[0].querySelector(
`[data-testid="${TEST_IDS.DIALOG_COUNT}"]`,
).textContent;
const manager2Count = testComponents[1].querySelector(
`[data-testid="${TEST_IDS.DIALOG_COUNT}"]`,
).textContent;

expect(manager1Count).toBe('1');
expect(manager2Count).toBe('0');

act(() => {
screen.getAllByTestId(TEST_IDS.OPEN_DIALOG)[1].click();
});

const manager1CountAfter = testComponents[0].querySelector(
`[data-testid="${TEST_IDS.DIALOG_COUNT}"]`,
).textContent;
const manager2CountAfter = testComponents[1].querySelector(
`[data-testid="${TEST_IDS.DIALOG_COUNT}"]`,
).textContent;

expect(manager1CountAfter).toBe('1');
expect(manager2CountAfter).toBe('1');
});

it('does not retrieve dialog manager only by dialog id', async () => {
render(
<DialogManagerProvider id={MANAGER_1_ID}>
<DialogTestComponent dialogId='manager-1-dialog' managerId={MANAGER_1_ID} />
<DialogManagerProvider id={MANAGER_2_ID}>
<TestComponent dialogId='manager-1-dialog' />
</DialogManagerProvider>
</DialogManagerProvider>,
);

await act(() => {
fireEvent.click(screen.getByTestId(TEST_IDS.OPEN_DIALOG));
});

await waitFor(async () => {
expect(await screen.findByTestId(TEST_IDS.DIALOG_COUNT)).toHaveTextContent('0');
const managerId = screen.getByTestId(TEST_IDS.MANAGER_ID_DISPLAY).textContent;
expect(managerId).toBe(MANAGER_2_ID);
});
});

it('uses the manager from the nearest context provider when manager is not found by id', () => {
render(
<DialogManagerProvider id={MANAGER_1_ID}>
<TestComponent dialogManagerId='non-existent' />
</DialogManagerProvider>,
);

expect(screen.getByTestId(TEST_IDS.MANAGER_ID_DISPLAY)).toHaveTextContent(
MANAGER_1_ID,
);
});
});
});
Loading