/**
* 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 { render, screen, act } from '@testing-library/react';
import { observer } from '../observer';
import { useFlowContext } from '../../FlowContextProvider';
import { observable } from '@formily/reactive';
import { vi } from 'vitest';
import { sleep } from '@nocobase/test/client';
// Mock useFlowContext
vi.mock('../../FlowContextProvider', async () => {
const actual = await vi.importActual('../../FlowContextProvider');
return {
...actual,
useFlowContext: vi.fn(),
};
});
describe('observer', () => {
beforeEach(() => {
(useFlowContext as any).mockReturnValue({});
});
afterEach(() => {
vi.clearAllMocks();
});
it('should render component', () => {
const Component = observer(() =>
Hello World
);
render();
expect(screen.getByText('Hello World')).toBeInTheDocument();
});
it('should update when observable changes', async () => {
const model = observable({ count: 0 });
const Component = observer(() => Count: {model.count}
);
render();
expect(screen.getByText('Count: 0')).toBeInTheDocument();
act(() => {
model.count++;
});
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('should delay update when pageActive is false', async () => {
const model = observable({ count: 0 });
const pageActive = observable.ref(false);
const tabActive = observable.ref(true);
const context = {
pageActive: pageActive,
tabActive: tabActive,
};
(useFlowContext as any).mockReturnValue(context);
const Component = observer(() => Count: {model.count}
);
render();
expect(screen.getByText('Count: 0')).toBeInTheDocument();
// Update model, but pageActive is false
act(() => {
model.count++;
});
await sleep(30);
// Should not update yet
expect(screen.getByText('Count: 0')).toBeInTheDocument();
expect(screen.queryByText('Count: 1')).not.toBeInTheDocument();
// Now make pageActive true
act(() => {
pageActive.value = true;
});
await sleep(30);
// Should update now
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('should delay update when tabActive is false', async () => {
const model = observable({ count: 0 });
const pageActive = observable.ref(true);
const tabActive = observable.ref(false);
const context = {
pageActive: pageActive,
tabActive: tabActive,
};
(useFlowContext as any).mockReturnValue(context);
const Component = observer(() => Count: {model.count}
);
render();
expect(screen.getByText('Count: 0')).toBeInTheDocument();
// Update model, but tabActive is false
act(() => {
model.count++;
});
await sleep(30);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
expect(screen.queryByText('Count: 1')).not.toBeInTheDocument();
// Now make tabActive true
act(() => {
tabActive.value = true;
});
await sleep(30);
// Should update now
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('should update immediately when pageActive and tabActive are true', async () => {
const model = observable({ count: 0 });
const pageActive = observable.ref(true);
const tabActive = observable.ref(true);
const context = {
pageActive: pageActive,
tabActive: tabActive,
};
(useFlowContext as any).mockReturnValue(context);
const Component = observer(() => Count: {model.count}
);
render();
expect(screen.getByText('Count: 0')).toBeInTheDocument();
act(() => {
model.count++;
});
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('should handle undefined tabActive as true (backward compatibility or default)', async () => {
const model = observable({ count: 0 });
const pageActive = observable.ref(true);
// tabActive is undefined in context
const context = {
pageActive: pageActive,
};
(useFlowContext as any).mockReturnValue(context);
const Component = observer(() => Count: {model.count}
);
render();
expect(screen.getByText('Count: 0')).toBeInTheDocument();
act(() => {
model.count++;
});
await sleep(30);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('should respect view inputArgs for initial check', async () => {
const model = observable({ count: 0 });
const context = {
view: {
inputArgs: { pageActive: false },
},
};
(useFlowContext as any).mockReturnValue(context);
const Component = observer(() => Count: {model.count}
);
render();
expect(screen.getByText('Count: 0')).toBeInTheDocument();
act(() => {
model.count++;
});
await sleep(30);
// Should NOT update because pageActive is false (from view.inputArgs)
expect(screen.getByText('Count: 0')).toBeInTheDocument();
expect(screen.queryByText('Count: 1')).not.toBeInTheDocument();
});
it('should flush pending update without TDZ error when context becomes active before timer callback runs', async () => {
vi.useFakeTimers();
try {
const model = observable({ count: 0 });
const pageActive = observable.ref(false);
const tabActive = observable.ref(true);
const context = {
pageActive,
tabActive,
};
(useFlowContext as any).mockReturnValue(context);
const Component = observer(() => Count: {model.count}
);
render();
expect(screen.getByText('Count: 0')).toBeInTheDocument();
act(() => {
model.count++;
pageActive.value = true;
});
await act(async () => {
await vi.runAllTimersAsync();
});
expect(screen.getByText('Count: 1')).toBeInTheDocument();
} finally {
vi.useRealTimers();
}
});
it('should cleanup pending timer and listener on unmount', async () => {
vi.useFakeTimers();
try {
const model = observable({ count: 0 });
const pageActive = observable.ref(false);
const tabActive = observable.ref(true);
const renderSpy = vi.fn();
const context = {
pageActive,
tabActive,
};
(useFlowContext as any).mockReturnValue(context);
const Component = observer(() => {
renderSpy(model.count);
return Count: {model.count}
;
});
const { unmount } = render();
expect(renderSpy).toHaveBeenCalledTimes(1);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
act(() => {
model.count++;
});
unmount();
act(() => {
pageActive.value = true;
});
await act(async () => {
await vi.runAllTimersAsync();
});
expect(renderSpy).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
}
});
});