Skip to content

Commit 802c709

Browse files
authored
Merge pull request #103 from simonsobs/dev
Update tests, fix bugs
2 parents f21fe0f + b42aae5 commit 802c709

File tree

5 files changed

+217
-57
lines changed

5 files changed

+217
-57
lines changed

src/utils/monaco-editor/__tests__/editor.spec.ts

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,87 @@
11
import { describe, it, expect } from "vitest";
2-
import { ref, nextTick } from "vue";
2+
import { ref, nextTick, unref, isRef } from "vue";
3+
import type { Ref } from "vue";
4+
5+
import fc from "fast-check";
36

47
import { withAsyncSetup } from "@/tests/test-utils";
58
import { useMonacoEditor } from "..";
69

7-
describe("useMonacoEditor()", () => {
8-
it("Editor is created", async () => {
9-
let result!: Awaited<ReturnType<typeof useMonacoEditor>>;
10-
const element = ref(document.createElement("div"));
11-
const source = ref("# Hello, world!");
12-
await withAsyncSetup(async () => {
13-
result = await useMonacoEditor({ element, source });
14-
});
15-
const { editor, model } = result;
16-
expect(editor.value).toBeDefined();
17-
expect(model.value?.getValue()).toBe("# Hello, world!");
10+
import { fcSource, fcLanguage } from "./model.spec";
11+
12+
const fcElement = () =>
13+
fc
14+
.oneof(
15+
fc.integer().map(() => document.createElement("div")),
16+
fc.constant(undefined)
17+
)
18+
.chain(
19+
(element): fc.Arbitrary<HTMLElement | Ref<HTMLElement | undefined>> =>
20+
element === undefined
21+
? fc.constant(ref(element))
22+
: fc.oneof(fc.constant(element), fc.constant(ref(element)))
23+
);
24+
25+
const fcMode = () =>
26+
fc.option(fc.constantFrom("viewer" as const, "editor" as const), { nil: undefined });
27+
28+
const fcUseMonacoEditorOptions = () =>
29+
fc.record(
30+
{
31+
element: fcElement(),
32+
mode: fcMode(),
33+
source: fcSource(),
34+
language: fcLanguage(),
35+
},
36+
{ requiredKeys: ["element"] }
37+
);
38+
39+
describe("fcUseMonacoEditorOptions()", () => {
40+
it("Options of useMonacoEditor() are generated", () => {
41+
fc.assert(
42+
fc.property(fcUseMonacoEditorOptions(), (options) => {
43+
expect(options).toBeDefined();
44+
return true;
45+
})
46+
);
1847
});
48+
});
1949

20-
it("The 'ready' is awaited", async () => {
21-
let result!: ReturnType<typeof useMonacoEditor>;
22-
const element = ref(document.createElement("div"));
23-
const source = ref("# Hello, world!");
24-
await withAsyncSetup(async () => {
25-
result = useMonacoEditor({ element, source });
26-
});
27-
const { editor, model, ready } = result;
28-
await ready;
29-
expect(editor.value).toBeDefined();
30-
expect(model.value?.getValue()).toBe("# Hello, world!");
50+
describe("useMonacoEditor()", () => {
51+
it("Property test", async () => {
52+
await fc.assert(
53+
fc.asyncProperty(
54+
fcUseMonacoEditorOptions(),
55+
fc.boolean(),
56+
async (options, early) => {
57+
let editor!: ReturnType<typeof useMonacoEditor>["editor"];
58+
let model!: ReturnType<typeof useMonacoEditor>["model"];
59+
let source!: ReturnType<typeof useMonacoEditor>["source"];
60+
let mode!: ReturnType<typeof useMonacoEditor>["mode"];
61+
let ready!: ReturnType<typeof useMonacoEditor>["ready"];
62+
const { wrapper } = await withAsyncSetup(async () => {
63+
({ editor, model, source, mode, ready } = early
64+
? useMonacoEditor(options)
65+
: await useMonacoEditor(options));
66+
});
67+
if (early) await ready;
68+
if (isRef(options.element) && options.element.value === undefined) {
69+
options.element.value = document.createElement("div");
70+
}
71+
await nextTick();
72+
expect(editor.value).toBeDefined();
73+
74+
const expectedSource = unref(options.source) ?? "";
75+
expect(source.value).toBe(expectedSource);
76+
expect(model.value?.getValue()).toBe(expectedSource);
77+
78+
const expectedMode = unref(options.mode) ?? "viewer";
79+
expect(mode.value).toBe(expectedMode);
80+
81+
wrapper.unmount();
82+
}
83+
)
84+
);
3185
});
3286

3387
it("The mode changes between 'viewer' and 'editor'", async () => {

src/utils/monaco-editor/__tests__/model.spec.ts

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,91 @@
11
import { describe, it, expect } from "vitest";
2-
import { ref, nextTick } from "vue";
2+
import { ref, nextTick, unref, isRef } from "vue";
3+
4+
import fc from "fast-check";
35

46
import { useModel } from "..";
57

6-
describe("useModel()", () => {
7-
it("call without arguments", async () => {
8-
const { model, source } = await useModel();
9-
expect(source.value).toBe("");
10-
expect(model.value?.getValue()).toBe("");
8+
const fcText = () =>
9+
fc.array(fc.oneof(fc.lorem(), fc.constant("")), {
10+
maxLength: 100,
11+
size: "max",
1112
});
1213

13-
it("call with string source", async () => {
14-
const source = "# Hello, world!";
15-
const { model } = await useModel({ source });
16-
expect(model.value?.getValue()).toBe("# Hello, world!");
14+
export const fcSource = () =>
15+
fc.option(
16+
fc.oneof(
17+
fcText().map((lines) => lines.join("\n")),
18+
fcText().map((lines) => ref(lines.join("\n")))
19+
),
20+
{ nil: undefined }
21+
);
22+
23+
export const fcLanguage = () =>
24+
fc.option(fc.constantFrom("python", "typescript"), {
25+
nil: undefined,
1726
});
1827

19-
it("call with ref source", async () => {
20-
const source = ref("# Hello, world!");
21-
const { model, source: sourceReturned } = await useModel({ source });
22-
expect(sourceReturned === source).toBe(true);
23-
expect(model.value?.getValue()).toBe("# Hello, world!");
28+
export const fcUseModelOptions = () =>
29+
fc.record(
30+
{
31+
source: fcSource(),
32+
language: fcLanguage(),
33+
},
34+
{ withDeletedKeys: true }
35+
);
36+
37+
describe("fcUseModelOptions()", () => {
38+
it("Options of useModel() are generated", () => {
39+
fc.assert(
40+
fc.property(fcUseModelOptions(), (options) => {
41+
expect(options).toBeDefined();
42+
return true;
43+
})
44+
);
45+
});
46+
});
47+
48+
const fcUseModelArgs = () =>
49+
fc.option(
50+
fcUseModelOptions().map((options) => [options]),
51+
{ nil: [] }
52+
);
53+
54+
function assertDefined<T>(value: T): asserts value is NonNullable<T> {
55+
expect(value).toBeDefined();
56+
}
57+
58+
describe("useModel()", () => {
59+
it("Property test", async () => {
60+
await fc.assert(
61+
fc.asyncProperty(fcUseModelArgs(), fc.boolean(), async (args, early) => {
62+
const { model, source, dispose, ready } = early
63+
? useModel(...args)
64+
: await useModel(...args);
65+
if (early) await ready;
66+
67+
const model_ = model.value;
68+
assertDefined(model_);
69+
70+
const options = args?.[0];
71+
72+
const expectedLanguage = options?.language ?? "python";
73+
expect(model_.getLanguageId()).toBe(expectedLanguage);
74+
75+
if (isRef(options?.source)) {
76+
expect(source.value).toBe(options?.source.value);
77+
}
78+
79+
const expectedValue = unref(options?.source ?? "");
80+
expect(model_.getValue()).toBe(expectedValue);
81+
82+
dispose();
83+
}),
84+
{ verbose: true, numRuns: 10 } // NOTE: Error with larger numRuns
85+
);
2486
});
2587

26-
it("source is reactive", async () => {
88+
it("Source is reactive", async () => {
2789
const source = ref("# Hello, world!");
2890
const { model } = await useModel({ source });
2991
expect(model.value?.getValue()).toBe("# Hello, world!");
@@ -32,7 +94,7 @@ describe("useModel()", () => {
3294
expect(model.value?.getValue()).toBe("# New source");
3395
});
3496

35-
it("edit model", async () => {
97+
it("Edit model", async () => {
3698
const source = ref("# Hello, world!");
3799
const { model } = await useModel({ source });
38100
expect(model.value?.getValue()).toBe("# Hello, world!");

src/utils/monaco-editor/editor.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const modelOptionsMap: Record<Mode, Monaco.editor.IEditorOptions> = {
4646
};
4747

4848
export interface UseMonacoEditorOptions extends UseModelOptions {
49-
element: MaybeRef<HTMLElement | undefined>;
49+
element: HTMLElement | Ref<HTMLElement | undefined>;
5050
mode?: MaybeRef<Mode>;
5151
}
5252

@@ -70,7 +70,13 @@ export function useMonacoEditor(
7070

7171
const mode = ref(mode_ ?? defaultMode);
7272

73-
const { model, source, beforeSetValue, afterSetValue } = useModel(modelOptions);
73+
const {
74+
model,
75+
source,
76+
beforeSetValue,
77+
afterSetValue,
78+
ready: readyModel,
79+
} = useModel(modelOptions);
7480

7581
const editor = shallowRef<Monaco.editor.IStandaloneCodeEditor>();
7682

@@ -81,17 +87,15 @@ export function useMonacoEditor(
8187

8288
watchEffect(() => {
8389
if (!isMounted.value) return;
84-
const ele = unref(element);
85-
if (!ele) {
86-
console.error("element is undefined");
87-
return;
88-
}
90+
91+
const htmlElement = unref(element);
92+
if (!htmlElement) return;
93+
8994
const model_ = unref(model);
9095
if (!model_) return;
91-
editor.value = monaco.value?.editor.create(ele, {
92-
model: model_,
93-
...editorOptionsBase,
94-
});
96+
97+
const options = { model: model_, ...editorOptionsBase };
98+
editor.value = monaco.value?.editor.create(htmlElement, options);
9599
});
96100

97101
watchEffect(() => {
@@ -118,6 +122,7 @@ export function useMonacoEditor(
118122
async function loadMonaco() {
119123
await useColorThemeOnMonacoEditor();
120124
monaco.value = await import("monaco-editor");
125+
await readyModel;
121126
}
122127

123128
const ready = loadMonaco();

src/utils/monaco-editor/model.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,46 @@ const _default: Required<UseModelOptions> = {
1818
sourceUpdateMaxWaitMilliseconds: 100,
1919
};
2020

21+
function resolveOptions(options?: UseModelOptions) {
22+
// Remove `undefined` so default has precedence.
23+
const given = Object.fromEntries(
24+
Object.entries(options || {}).filter(([, v]) => v !== undefined)
25+
);
26+
const defaultApplied = { ..._default, ...given };
27+
28+
// Turn `MaybeRef` into `Ref`.
29+
const { source: source_, ...rest } = defaultApplied;
30+
const source: Ref<string> = ref(source_);
31+
return { source, ...rest };
32+
}
33+
2134
interface _UseModelReturn {
2235
model: ShallowRef<Monaco.editor.ITextModel | undefined>;
2336
source: Ref<string>;
2437
beforeSetValue: (fn: () => void) => void;
2538
afterSetValue: (fn: () => void) => void;
39+
dispose: () => void;
2640
ready: Promise<void>;
2741
}
2842

2943
type UseModelReturn = _UseModelReturn & PromiseLike<_UseModelReturn>;
3044

3145
export function useModel(options?: UseModelOptions): UseModelReturn {
3246
const {
33-
source: source_,
47+
source,
3448
language,
3549
sourceUpdateDelayMilliseconds,
3650
sourceUpdateMaxWaitMilliseconds,
37-
} = { ..._default, ...options };
51+
} = resolveOptions(options);
3852

3953
const monaco = ref<typeof Monaco>();
4054
const model = shallowRef<Monaco.editor.ITextModel>();
4155

42-
const source = ref(source_);
43-
4456
const beforeSetValue = createEventHook<null>();
4557
const afterSetValue = createEventHook<null>();
4658

4759
// Update model when source changes.
48-
watchEffect(() => {
60+
const stop = watchEffect(() => {
4961
if (!model.value) return;
5062
if (source.value === model.value.getValue()) return;
5163
beforeSetValue.trigger(null);
@@ -63,10 +75,35 @@ export function useModel(options?: UseModelOptions): UseModelReturn {
6375
{ maxWait: sourceUpdateMaxWaitMilliseconds }
6476
);
6577

78+
function registerLanguage(monaco: typeof Monaco, language: string) {
79+
// Register the language if it is not registered.
80+
// Many languages are typically registered in browsers.
81+
// Only `plaintext` is registered in Vitest because of `vitest.config.ts`.
82+
if (!language) return;
83+
if (
84+
monaco.languages
85+
.getLanguages()
86+
.map((lang) => lang.id)
87+
.includes(language)
88+
)
89+
return;
90+
monaco.languages.register({ id: language });
91+
}
92+
93+
function dispose() {
94+
stop();
95+
// TODO: Cancel debounced function. https://github.com/vueuse/vueuse/pull/4561
96+
disposeOnDidChangeContent?.dispose();
97+
if (model.value) model.value.dispose();
98+
}
99+
100+
let disposeOnDidChangeContent: Monaco.IDisposable | undefined;
101+
66102
async function loadMonaco() {
67103
monaco.value = await import("monaco-editor");
104+
registerLanguage(monaco.value, language);
68105
model.value = monaco.value.editor.createModel(source.value, language);
69-
model.value.onDidChangeContent(updateSource);
106+
disposeOnDidChangeContent = model.value.onDidChangeContent(updateSource);
70107
}
71108

72109
const ready = loadMonaco();
@@ -76,6 +113,7 @@ export function useModel(options?: UseModelOptions): UseModelReturn {
76113
source,
77114
beforeSetValue: beforeSetValue.on,
78115
afterSetValue: afterSetValue.on,
116+
dispose,
79117
ready,
80118
};
81119

vitest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default mergeConfig(
1212
root: fileURLToPath(new URL("./", import.meta.url)),
1313
setupFiles: ["./src/tests/setup.ts"],
1414
includeSource: ["src/**/*.{js,ts}"],
15+
testTimeout: 30_000,
1516
alias: [
1617
{
1718
// https://github.com/vitest-dev/vitest/discussions/1806#discussioncomment-3570047

0 commit comments

Comments
 (0)