Skip to content

Commit 29785fc

Browse files
committed
feat: accept DurationLike values and use assertions to avoid rendering an invalid component
1 parent 23b74ba commit 29785fc

File tree

4 files changed

+64
-58
lines changed

4 files changed

+64
-58
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`ObsidianMarkdown > should throw when 'prop.delay' is invalid 1`] = `[Error: Unknown duration argument 0 of type string]`;
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,89 @@
11
import { render } from "@testing-library/preact";
2-
import { Duration } from "luxon";
2+
import { DurationLike } from "luxon";
3+
import { OmitByValue } from "utility-types";
34
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
45

56
import { App, Component, MarkdownRenderer } from "@/lib/obsidian/types";
67

7-
import { ObsidianMarkdown } from "../obsidian";
8+
import { ObsidianMarkdown, ObsidianMarkdownProps } from "../obsidian";
89

910
vi.mock("@/lib/obsidian/types");
1011
afterEach(() => vi.restoreAllMocks());
1112

1213
describe(`${ObsidianMarkdown.name}`, () => {
14+
const required: OmitByValue<ObsidianMarkdownProps, undefined> = {
15+
app: new App(),
16+
component: new Component(),
17+
sourcePath: "/vault/diary.md",
18+
markdown: "Foo",
19+
};
20+
1321
beforeAll(() => vi.useFakeTimers());
1422
afterAll(() => vi.useRealTimers());
1523

1624
it("should rerender when markdown changes stay stable for a while", async () => {
17-
const props = {
18-
app: new App(),
19-
component: new Component(),
20-
sourcePath: "/vault/diary.md",
21-
delay: Duration.fromMillis(500),
22-
} as const;
23-
24-
const { rerender } = render(<ObsidianMarkdown {...props} markdown="Apple" />);
25+
const { rerender } = render(<ObsidianMarkdown {...required} markdown="Apple" delay={{ milliseconds: 500 }} />);
2526
await vi.runAllTimersAsync();
26-
rerender(<ObsidianMarkdown {...props} markdown="Banana" />);
27+
rerender(<ObsidianMarkdown {...required} markdown="Banana" delay={{ milliseconds: 500 }} />);
2728
await vi.advanceTimersByTimeAsync(300);
28-
rerender(<ObsidianMarkdown {...props} markdown="Banana" />);
29+
rerender(<ObsidianMarkdown {...required} markdown="Banana" delay={{ milliseconds: 500 }} />);
2930
await vi.advanceTimersByTimeAsync(300);
3031

31-
const anySpanElement = expect.any(HTMLSpanElement);
3232
expect(MarkdownRenderer.render).toHaveBeenCalledTimes(2);
3333
expect(MarkdownRenderer.render).toHaveBeenCalledWith(
34-
props.app,
34+
required.app,
3535
"Apple",
36-
anySpanElement,
37-
props.sourcePath,
38-
props.component,
36+
expect.any(HTMLSpanElement),
37+
required.sourcePath,
38+
required.component,
3939
);
4040
expect(MarkdownRenderer.render).toHaveBeenCalledWith(
41-
props.app,
41+
required.app,
4242
"Banana",
43-
anySpanElement,
44-
props.sourcePath,
45-
props.component,
43+
expect.any(HTMLSpanElement),
44+
required.sourcePath,
45+
required.component,
4646
);
4747
});
4848

4949
it("should not rerender when markdown changes too frequently", async () => {
50-
const props = {
51-
app: new App(),
52-
component: new Component(),
53-
sourcePath: "/vault/diary.md",
54-
delay: Duration.fromMillis(500),
55-
} as const;
56-
57-
const { rerender } = render(<ObsidianMarkdown {...props} markdown="Apple" />);
50+
const { rerender } = render(<ObsidianMarkdown {...required} markdown="Apple" delay={{ milliseconds: 500 }} />);
5851
await vi.runAllTimersAsync();
59-
rerender(<ObsidianMarkdown {...props} markdown="Banana" />);
52+
rerender(<ObsidianMarkdown {...required} markdown="Banana" delay={{ milliseconds: 500 }} />);
6053
await vi.advanceTimersByTimeAsync(300);
61-
rerender(<ObsidianMarkdown {...props} markdown="Cherry" />);
54+
rerender(<ObsidianMarkdown {...required} markdown="Cherry" delay={{ milliseconds: 500 }} />);
6255
await vi.advanceTimersByTimeAsync(300);
6356

64-
const anySpanElement = expect.any(HTMLSpanElement);
6557
expect(MarkdownRenderer.render).toHaveBeenCalledTimes(1);
6658
expect(MarkdownRenderer.render).toHaveBeenCalledWith(
67-
props.app,
59+
required.app,
6860
"Apple",
69-
anySpanElement,
70-
props.sourcePath,
71-
props.component,
61+
expect.any(HTMLSpanElement),
62+
required.sourcePath,
63+
required.component,
7264
);
7365
expect(MarkdownRenderer.render).not.toHaveBeenCalledWith(
74-
props.app,
66+
required.app,
7567
"Banana",
76-
anySpanElement,
77-
props.sourcePath,
78-
props.component,
68+
expect.any(HTMLSpanElement),
69+
required.sourcePath,
70+
required.component,
7971
);
8072
expect(MarkdownRenderer.render).not.toHaveBeenCalledWith(
81-
props.app,
73+
required.app,
8274
"Cherry",
83-
anySpanElement,
84-
props.sourcePath,
85-
props.component,
75+
expect.any(HTMLSpanElement),
76+
required.sourcePath,
77+
required.component,
8678
);
8779
});
80+
81+
it("should not throw when default props get used", () => {
82+
expect(() => render(<ObsidianMarkdown {...required} />)).not.toThrow();
83+
});
84+
85+
it("should throw when 'prop.delay' is invalid", () => {
86+
const invalid = "0" as DurationLike;
87+
expect(() => render(<ObsidianMarkdown {...required} delay={invalid} />)).toThrowErrorMatchingSnapshot();
88+
});
8889
});
+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
import { Duration } from "luxon";
22

3-
export const MARKDOWN_RENDER_DEBOUNCE_TIME = Duration.fromDurationLike({ milliseconds: 500 });
3+
export const MARKDOWN_RENDER_DEBOUNCE_TIME = Duration.fromMillis(500);

src/render/preact/lib/obsidian.tsx

+15-13
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
1-
import { Duration } from "luxon";
1+
import { Duration, DurationLike } from "luxon";
22
import { createElement, FunctionalComponent, JSX } from "preact";
3-
import { useEffect, useRef } from "preact/hooks";
3+
import { useEffect, useMemo, useRef } from "preact/hooks";
44
import { useDebounceCallback } from "usehooks-ts";
55

66
import { App, Component, MarkdownRenderer } from "@/lib/obsidian/types";
7+
import { assertLuxonValidity } from "@/util/luxon-utils";
78

89
import { MARKDOWN_RENDER_DEBOUNCE_TIME } from "./obsidian.const";
910

1011
/**
11-
* Asynchronously renders the given markdown source code as Obsidian (the app) would.
12-
* Debounces values so that, ideally, renders have enough time to finish before being discarded for newer values.
13-
* @param props - configures how the markdown is rendered.
14-
* @returns component for asynchronously rendering the given markdown source code as Obsidian (the app) would.
12+
* @param props - props for the component.
13+
* @returns component that asynchronously renders the given markdown source code just as Obsidian (the app) would.
1514
*/
1615
export const ObsidianMarkdown: FunctionalComponent<ObsidianMarkdownProps> = (props) => {
17-
const { app, component, markdown, sourcePath, tagName = "span", delay = MARKDOWN_RENDER_DEBOUNCE_TIME } = props;
16+
const { app, component, markdown, sourcePath, tagName = "span" } = props;
17+
const delay = useMemo(() => Duration.fromDurationLike(props.delay ?? MARKDOWN_RENDER_DEBOUNCE_TIME), [props.delay]);
18+
assertLuxonValidity(delay);
19+
1820
const elRef = useRef<HTMLElement>();
19-
const render = useDebounceCallback(MarkdownRenderer.render, delay.toMillis());
21+
const renderObsidianMarkdown = useDebounceCallback(MarkdownRenderer.render, delay.toMillis());
2022

2123
useEffect(() => {
22-
const { current: el } = elRef;
24+
const el = elRef.current;
2325
if (el) {
24-
render(app, markdown, el, sourcePath, component);
25-
return render.cancel;
26+
renderObsidianMarkdown(app, markdown, el, sourcePath, component);
27+
return renderObsidianMarkdown.cancel;
2628
}
27-
}, [render, app, markdown, sourcePath, component]);
29+
}, [renderObsidianMarkdown, app, markdown, sourcePath, component]);
2830

2931
return createElement(tagName, { ref: elRef });
3032
};
@@ -42,5 +44,5 @@ export interface ObsidianMarkdownProps {
4244
/** The tag used to contain the rendered Markdown source code. */
4345
tagName?: keyof JSX.IntrinsicElements;
4446
/** Custom debounce time for renders. */
45-
delay?: Duration<true>;
47+
delay?: DurationLike;
4648
}

0 commit comments

Comments
 (0)