mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-07 11:21:11 +03:00
@@ -0,0 +1,196 @@
|
||||
import { h } from 'preact';
|
||||
import { set as setData } from 'idb-keyval';
|
||||
import { DarkModeProvider, useDarkMode, usePersistence } from '..';
|
||||
import { fireEvent, render, screen } from 'testing-library';
|
||||
import { useCallback } from 'preact/hooks';
|
||||
import * as WS from '../../api/ws';
|
||||
|
||||
function DarkModeChecker() {
|
||||
const { currentMode } = useDarkMode();
|
||||
return <div data-testid={currentMode}>{currentMode}</div>;
|
||||
}
|
||||
|
||||
describe('DarkMode', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(WS, 'WsProvider').mockImplementation(({ children }) => children);
|
||||
});
|
||||
|
||||
test('uses media by default', async () => {
|
||||
render(
|
||||
<DarkModeProvider>
|
||||
<DarkModeChecker />
|
||||
</DarkModeProvider>
|
||||
);
|
||||
const el = await screen.findByTestId('media');
|
||||
expect(el).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('uses the mode stored in idb - dark', async () => {
|
||||
setData('darkmode', 'dark');
|
||||
render(
|
||||
<DarkModeProvider>
|
||||
<DarkModeChecker />
|
||||
</DarkModeProvider>
|
||||
);
|
||||
const el = await screen.findByTestId('dark');
|
||||
expect(el).toBeInTheDocument();
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
|
||||
test('uses the mode stored in idb - light', async () => {
|
||||
setData('darkmode', 'light');
|
||||
render(
|
||||
<DarkModeProvider>
|
||||
<DarkModeChecker />
|
||||
</DarkModeProvider>
|
||||
);
|
||||
const el = await screen.findByTestId('light');
|
||||
expect(el).toBeInTheDocument();
|
||||
expect(document.body.classList.contains('dark')).toBe(false);
|
||||
});
|
||||
|
||||
test('allows updating the mode', async () => {
|
||||
setData('darkmode', 'dark');
|
||||
|
||||
function Updater() {
|
||||
const { setDarkMode } = useDarkMode();
|
||||
const handleClick = useCallback(() => {
|
||||
setDarkMode('light');
|
||||
}, [setDarkMode]);
|
||||
return <div onClick={handleClick}>click me</div>;
|
||||
}
|
||||
|
||||
render(
|
||||
<DarkModeProvider>
|
||||
<DarkModeChecker />
|
||||
<Updater />
|
||||
</DarkModeProvider>
|
||||
);
|
||||
|
||||
const dark = await screen.findByTestId('dark');
|
||||
expect(dark).toBeInTheDocument();
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
|
||||
const button = await screen.findByText('click me');
|
||||
fireEvent.click(button);
|
||||
|
||||
const light = await screen.findByTestId('light');
|
||||
expect(light).toBeInTheDocument();
|
||||
expect(document.body.classList.contains('dark')).toBe(false);
|
||||
});
|
||||
|
||||
test('when using media, matches on preference', async () => {
|
||||
setData('darkmode', 'media');
|
||||
vi.spyOn(window, 'matchMedia').mockImplementation((query) => {
|
||||
if (query === '(prefers-color-scheme: dark)') {
|
||||
return { matches: true, addEventListener: vi.fn(), removeEventListener: vi.fn() };
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected query to matchMedia: ${query}`);
|
||||
});
|
||||
render(
|
||||
<DarkModeProvider>
|
||||
<DarkModeChecker />
|
||||
</DarkModeProvider>
|
||||
);
|
||||
|
||||
const el = await screen.findByTestId('dark');
|
||||
expect(el).toBeInTheDocument();
|
||||
expect(document.body.classList.contains('dark')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePersistence', () => {
|
||||
test('returns a defaultValue initially', async () => {
|
||||
function Component() {
|
||||
const [value, , loaded] = usePersistence('tacos', 'my-default');
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loaded">{loaded ? 'loaded' : 'not loaded'}</div>
|
||||
<div data-testid="value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Component />);
|
||||
|
||||
expect(screen.getByTestId('loaded')).toMatchInlineSnapshot(`
|
||||
<div
|
||||
data-testid="loaded"
|
||||
>
|
||||
not loaded
|
||||
</div>
|
||||
`);
|
||||
expect(screen.getByTestId('value')).toMatchInlineSnapshot(`
|
||||
<div
|
||||
data-testid="value"
|
||||
>
|
||||
my-default
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
test.skip('updates with the previously-persisted value', async () => {
|
||||
setData('tacos', 'are delicious');
|
||||
|
||||
function Component() {
|
||||
const [value, , loaded] = usePersistence('tacos', 'my-default');
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="loaded">{loaded ? 'loaded' : 'not loaded'}</div>
|
||||
<div data-testid="value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Component />);
|
||||
|
||||
await screen.findByText('loaded');
|
||||
|
||||
expect(screen.getByTestId('loaded')).toMatchInlineSnapshot(`
|
||||
<div
|
||||
data-testid="loaded"
|
||||
>
|
||||
loaded
|
||||
</div>
|
||||
`);
|
||||
expect(screen.getByTestId('value')).toMatchInlineSnapshot(`
|
||||
<div
|
||||
data-testid="value"
|
||||
>
|
||||
are delicious
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
test('can be updated manually', async () => {
|
||||
setData('darkmode', 'are delicious');
|
||||
|
||||
function Component() {
|
||||
const [value, setValue] = usePersistence('tacos', 'my-default');
|
||||
const handleClick = useCallback(() => {
|
||||
setValue('super delicious');
|
||||
}, [setValue]);
|
||||
return (
|
||||
<div>
|
||||
<div onClick={handleClick}>click me</div>
|
||||
<div data-testid="value">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(<Component />);
|
||||
|
||||
const button = await screen.findByText('click me');
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(screen.getByTestId('value')).toMatchInlineSnapshot(`
|
||||
<div
|
||||
data-testid="value"
|
||||
>
|
||||
super delicious
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { h, createContext } from 'preact';
|
||||
import { get as getData, set as setData } from 'idb-keyval';
|
||||
import { useCallback, useContext, useEffect, useLayoutEffect, useState } from 'preact/hooks';
|
||||
|
||||
const DarkMode = createContext(null);
|
||||
|
||||
export function DarkModeProvider({ children }) {
|
||||
const [persistedMode, setPersistedMode] = useState(null);
|
||||
const [currentMode, setCurrentMode] = useState(persistedMode !== 'media' ? persistedMode : null);
|
||||
|
||||
const setDarkMode = useCallback(
|
||||
(value) => {
|
||||
setPersistedMode(value);
|
||||
setData('darkmode', value);
|
||||
setCurrentMode(value);
|
||||
},
|
||||
[setPersistedMode]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const darkmode = await getData('darkmode');
|
||||
setDarkMode(darkmode || 'media');
|
||||
}
|
||||
|
||||
load();
|
||||
}, [setDarkMode]);
|
||||
|
||||
const handleMediaMatch = useCallback(
|
||||
({ matches }) => {
|
||||
if (matches) {
|
||||
setCurrentMode('dark');
|
||||
} else {
|
||||
setCurrentMode('light');
|
||||
}
|
||||
},
|
||||
[setCurrentMode]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (persistedMode !== 'media') {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
query.addEventListener('change', handleMediaMatch);
|
||||
handleMediaMatch(query);
|
||||
}, [persistedMode, handleMediaMatch]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (currentMode === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
} else {
|
||||
document.body.classList.remove('dark');
|
||||
}
|
||||
}, [currentMode]);
|
||||
|
||||
return !persistedMode ? null : (
|
||||
<DarkMode.Provider value={{ currentMode, persistedMode, setDarkMode }}>{children}</DarkMode.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useDarkMode() {
|
||||
return useContext(DarkMode);
|
||||
}
|
||||
|
||||
const Drawer = createContext(null);
|
||||
|
||||
export function DrawerProvider({ children }) {
|
||||
const [showDrawer, setShowDrawer] = useState(false);
|
||||
|
||||
return <Drawer.Provider value={{ showDrawer, setShowDrawer }}>{children}</Drawer.Provider>;
|
||||
}
|
||||
|
||||
export function useDrawer() {
|
||||
return useContext(Drawer);
|
||||
}
|
||||
|
||||
export function usePersistence(key, defaultValue = undefined) {
|
||||
const [value, setInternalValue] = useState(defaultValue);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const setValue = useCallback(
|
||||
(value) => {
|
||||
setInternalValue(value);
|
||||
async function update() {
|
||||
await setData(key, value);
|
||||
}
|
||||
|
||||
update();
|
||||
},
|
||||
[key]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setLoaded(false);
|
||||
setInternalValue(defaultValue);
|
||||
|
||||
async function load() {
|
||||
const value = await getData(key);
|
||||
if (typeof value !== 'undefined') {
|
||||
setValue(value);
|
||||
}
|
||||
setLoaded(true);
|
||||
}
|
||||
|
||||
load();
|
||||
}, [key, defaultValue, setValue]);
|
||||
|
||||
return [value, setValue, loaded];
|
||||
}
|
||||
Reference in New Issue
Block a user