Skip to content

Commit

Permalink
Fix vue tests, add default scope lease extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
loganvolkers authored Nov 23, 2023
1 parent c56f1d4 commit 49dd7e6
Show file tree
Hide file tree
Showing 9 changed files with 746 additions and 162 deletions.
696 changes: 550 additions & 146 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"@astrojs/language-server": "^2.3.3",
"@size-limit/preset-small-lib": "^9.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/vue": "^7.0.0",
"@testing-library/vue": "^8.0.1",
"@types/react": "^17",
"@vitejs/plugin-vue": "^4.2.3",
"@vitest/coverage-v8": "^0.34.4",
Expand Down
9 changes: 9 additions & 0 deletions src/vanilla/getDefaultInjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ export const getDefaultInjector = () => {

throw new Error(ErrorInvalidGlobalInjector);
};

/**
* Resets the globally defined default injector
*
* Useful for tests
*/
export const resetDefaultInjector = () => {
(globalThis as any)[DefaultInjector] = createInjector();
};
2 changes: 1 addition & 1 deletion src/vanilla/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "./ComponentScope";
export { getDefaultInjector } from "./getDefaultInjector";
export { getDefaultInjector, resetDefaultInjector } from "./getDefaultInjector";
export * from "./injector";
export { onMount, onUnmount, use } from "./lifecycle";
export * from "./molecule";
Expand Down
25 changes: 25 additions & 0 deletions src/vue/testing/ScopeProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineComponent, h, type Component } from "vue";
import type { AnyScopeTuple } from "../../vanilla/internal/internal-types";
import { provideScope } from "../provideScopes";

export const ScopeProvider = defineComponent({
props: {
tuple: null,
},
setup(props) {
provideScope(props.tuple);
},
template: `<div><slot/></div>`,
});

export const createProvider = (tuple: AnyScopeTuple): Component => {
return {
props: {},
render() {
// <ScopeProvider><slot /></ScopeProvider>
return h(ScopeProvider, { tuple }, () => this.$slots.default());
},
};
};

export default ScopeProvider;
15 changes: 7 additions & 8 deletions src/vue/testing/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// test-utils.js
import { render, type RenderOptions } from "@testing-library/vue";
import { defineComponent, h, shallowRef, toRef, VueElement } from "vue";
import { defineComponent, h, shallowRef, toRef, type Component } from "vue";

/**
* Wraps a composable into a component with instrumentation
Expand All @@ -11,11 +11,8 @@ import { defineComponent, h, shallowRef, toRef, VueElement } from "vue";
* @param composable - the composable to test
* @returns a component that is renderable
*/
export function wrap<T>(
composable: () => T,
options?: { Wrapper: VueElement },
) {
const innerComponent = defineComponent({
export function wrap<T>(composable: () => T, options?: { Wrapper: Component }) {
const innerComponent: Component = defineComponent({
props: {
result: {
required: true,
Expand All @@ -28,14 +25,16 @@ export function wrap<T>(
},
});

const component = options?.Wrapper
const component: Component = options?.Wrapper
? /**
* When there is a wrapper, then use JSX support from vue to create
* it, and pass the composable in the default slot
*
* https://vuejs.org/guide/extras/render-function.html#passing-slots
*/
() => h(options?.Wrapper, null, { default: () => h(innerComponent) })
(props) => {
return h(options?.Wrapper, () => h(innerComponent, props));
}
: /**
* No wrapper, so use the component as it is
*/
Expand Down
142 changes: 142 additions & 0 deletions src/vue/useMolecule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { resetDefaultInjector } from "../vanilla/getDefaultInjector";
import { CountMolecule, lifecycle, userScope } from "./testing/CountMolecule";
import { createProvider } from "./testing/ScopeProvider";
import { wrap } from "./testing/test-utils";
import { useMolecule } from "./useMolecule";

beforeEach(() => {
/**
* Prevents default scopes getting cached for too long
*/
resetDefaultInjector();
});

describe.each([
{ userId: "[email protected]", case: "Regular email" },
{ userId: "[email protected]", case: "Different email" },
{ userId: "", case: "Empty string" },
{ userId: " ", case: "White space" },
{ userId: userScope.defaultValue, case: "Default value" },
])("Two branches, one molecule value. $case", ({ userId }) => {
const Wrapper = createProvider([userScope, userId]);
test("Same component", () => {
const counter = wrap(() => useMolecule(CountMolecule), { Wrapper });

lifecycle.expectUncalled();
const [result1, rendered1] = counter.render();

expect(result1.value?.username).toBe(userId);
lifecycle.expectActivelyMounted();

const [result2, rendered2] = counter.render();
expect(result2.value?.username).toBe(userId);

rendered1.unmount();

lifecycle.expectActivelyMounted();
rendered2.unmount();

lifecycle.expectToMatchCalls([userId]);
});
test("Different components", () => {
const counter1 = wrap(() => useMolecule(CountMolecule), {
Wrapper: createProvider([userScope, userId]),
});
const counter2 = wrap(() => useMolecule(CountMolecule), {
Wrapper: createProvider([userScope, userId]),
});

lifecycle.expectUncalled();
const [result1, rendered1] = counter1.render();

expect(result1.value?.username).toBe(userId);
lifecycle.expectActivelyMounted();

const [result2, rendered2] = counter2.render();
expect(result2.value?.username).toBe(userId);

rendered1.unmount();

lifecycle.expectActivelyMounted();
rendered2.unmount();

lifecycle.expectToMatchCalls([userId]);
});
});

test.each([
{
userId1: "[email protected]",
userId2: "[email protected]",
case: "Different emails",
},
{ userId1: "a", userId2: "A", case: "Case sensitive" },
{ userId1: "a", userId2: "", case: "Empty strings" },
{ userId1: " ", userId2: " ", case: "White space" },
{
userId1: userScope.defaultValue,
userId2: "two",
case: "Default value is first",
},
{
userId1: "one",
userId2: userScope.defaultValue,
case: "Default value is second",
},
])(
"Two molecule values for different scopes. $case",
({ userId1, userId2 }) => {
const counter1 = wrap(
() => {
return useMolecule(CountMolecule);
},
{
Wrapper: createProvider([userScope, userId1]),
},
);

const counter2 = wrap(
() => {
return useMolecule(CountMolecule);
},
{
Wrapper: createProvider([userScope, userId2]),
},
);

lifecycle.expectUncalled();
const [result1, rendered1] = counter1.render();

expect(result1.value?.username).toBe(userId1);
lifecycle.expectActivelyMounted();

const [result2, rendered2] = counter2.render();
expect(result2.value?.username).toBe(userId2);

expect(lifecycle.unmounts).not.toHaveBeenCalled();
rendered1.unmount();
rendered2.unmount();

lifecycle.expectToMatchCalls([userId1], [userId2]);
},
);

test("Default scope cleanup", () => {
const counter1 = wrap(() => useMolecule(CountMolecule));
const counter2 = wrap(() => useMolecule(CountMolecule));

lifecycle.expectUncalled();
const [result1, rendered1] = counter1.render();

expect(result1.value?.username).toBe(userScope.defaultValue);
lifecycle.expectActivelyMounted();

const [result2, rendered2] = counter2.render();
expect(result2.value?.username).toBe(userScope.defaultValue);

expect(lifecycle.unmounts).not.toHaveBeenCalled();
rendered1.unmount();
rendered2.unmount();

lifecycle.expectToMatchCalls([userScope.defaultValue]);
});
6 changes: 3 additions & 3 deletions src/vue/useMolecule.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MoleculeScopeOptions } from "../shared/MoleculeScopeOptions";
import type { MoleculeOrInterface } from "../vanilla";
import { useInjector } from "./useInjector";
import { useScopes } from "./useScopes";
import { useScopeSubscription } from "./useScopes";

/**
* Gets an instance of a provided value from a {@link MoleculeOrInterface}
Expand All @@ -14,7 +14,7 @@ export const useMolecule = <T>(
mol: MoleculeOrInterface<T>,
options?: MoleculeScopeOptions,
) => {
const scopes = useScopes(options);
const [scopes, _, context] = useScopeSubscription(options);
const injector = useInjector();
return injector.get(mol, ...scopes);
return injector.get(mol, context, ...scopes);
};
11 changes: 8 additions & 3 deletions src/vue/useScopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ import { useInjector } from "./useInjector";
export const useScopes = (
options: MoleculeScopeOptions = {},
): ScopeTuple<unknown>[] => {
const [memoizedTuples] = useScopeSubscription(options);
return memoizedTuples;
};

export const useScopeSubscription = (options: MoleculeScopeOptions = {}) => {
const tuples = getTuples(options);
const injector = useInjector();
const [memoizedTuples, unsub] = injector.useScopes(...tuples);
onUnmounted(unsub);
return memoizedTuples;
const result = injector.useScopes(...tuples);
onUnmounted(result[1]);
return result;
};

const getTuples = (
Expand Down

0 comments on commit 49dd7e6

Please sign in to comment.