Skip to content

Commit

Permalink
Merge pull request #276 from logto-io/charles-log-2569-create-logto-v…
Browse files Browse the repository at this point in the history
…ue-sdk

feat(vue): create vue sdk
  • Loading branch information
charIeszhao authored May 23, 2022
2 parents b10eba4 + 6a68267 commit 0c0d9e6
Show file tree
Hide file tree
Showing 10 changed files with 695 additions and 0 deletions.
10 changes: 10 additions & 0 deletions packages/vue/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
preset: 'ts-jest',
collectCoverageFrom: ['src/**/*.ts'],
coverageReporters: ['lcov', 'text-summary'],
testEnvironment: 'jsdom',
};

export default config;
57 changes: 57 additions & 0 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@logto/vue",
"version": "0.1.7",
"main": "./lib/index.js",
"exports": "./lib/index.js",
"typings": "./lib/index.d.ts",
"files": [
"lib"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/logto-io/js.git",
"directory": "packages/vue"
},
"scripts": {
"dev:tsc": "tsc -p tsconfig.build.json -w --preserveWatchOutput",
"preinstall": "npx only-allow pnpm",
"precommit": "lint-staged",
"build": "rm -rf lib/ && tsc -p tsconfig.build.json",
"lint": "eslint --ext .ts src",
"test": "jest",
"test:coverage": "jest --silent --coverage",
"prepack": "pnpm test"
},
"dependencies": {
"@logto/browser": "^0.1.7"
},
"devDependencies": {
"@jest/types": "^27.5.1",
"@silverhand/eslint-config": "^0.14.0",
"@silverhand/ts-config": "^0.14.0",
"@types/jest": "^27.4.1",
"eslint": "^8.9.0",
"jest": "^27.5.1",
"lint-staged": "^12.3.4",
"postcss": "^8.4.6",
"prettier": "^2.5.1",
"stylelint": "^14.8.2",
"ts-jest": "^27.0.4",
"typescript": "^4.6.2",
"vue": "^3.2.35"
},
"peerDependencies": {
"vue": ">=3.0.0"
},
"eslintConfig": {
"extends": "@silverhand",
"rules": {
"unicorn/prevent-abbreviations": ["error", { "replacements": { "ref": false }}]
}
},
"prettier": "@silverhand/eslint-config/.prettierrc",
"publishConfig": {
"access": "public"
}
}
2 changes: 2 additions & 0 deletions packages/vue/src/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const logtoInjectionKey = '@logto/vue';
export const contextInjectionKey = '@logto/vue:context';
64 changes: 64 additions & 0 deletions packages/vue/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import LogtoClient from '@logto/browser';
import { computed, ComputedRef, reactive, Ref, toRefs, UnwrapRef } from 'vue';

type LogtoContextProperties = {
logtoClient: LogtoClient | undefined;
isAuthenticated: boolean;
loadingCount: number;
error: Error | undefined;
};

export type Context = {
// Wrong type workaround. https://github.com/vuejs/core/issues/2981
logtoClient: Ref<UnwrapRef<LogtoClient | undefined>>;
isAuthenticated: Ref<boolean>;
loadingCount: Ref<number>;
error: Ref<Error | undefined>;
isLoading: ComputedRef<boolean>;
setError: (error: unknown, fallbackErrorMessage?: string | undefined) => void;
setIsAuthenticated: (isAuthenticated: boolean) => void;
setLoading: (isLoading: boolean) => void;
};

export const createContext = (client: LogtoClient): Context => {
const context = toRefs(
reactive<LogtoContextProperties>({
logtoClient: client,
isAuthenticated: client.isAuthenticated,
loadingCount: 0,
error: undefined,
})
);

const { isAuthenticated, loadingCount, error } = context;

const isLoading = computed(() => loadingCount.value > 0);

/* eslint-disable @silverhand/fp/no-mutation */
const setError = (_error: unknown, fallbackErrorMessage?: string) => {
if (_error instanceof Error) {
error.value = _error;
} else if (fallbackErrorMessage) {
error.value = new Error(fallbackErrorMessage);
}
};

const setLoading = (isLoading: boolean) => {
if (isLoading) {
loadingCount.value += 1;
} else {
loadingCount.value = Math.max(0, loadingCount.value - 1);
}
};

const setIsAuthenticated = (_isAuthenticated: boolean) => {
isAuthenticated.value = _isAuthenticated;
};
/* eslint-enable @silverhand/fp/no-mutation */

return { ...context, isLoading, setError, setLoading, setIsAuthenticated };
};

export const throwContextError = (): never => {
throw new Error('Must install Logto plugin first.');
};
161 changes: 161 additions & 0 deletions packages/vue/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import LogtoClient from '@logto/browser';
import { App, readonly } from 'vue';

import { useLogto, useHandleSignInCallback, createLogto } from '.';
import { contextInjectionKey, logtoInjectionKey } from './consts';
import { createContext } from './context';
import { createPluginMethods } from './plugin';

const isSignInRedirected = jest.fn(() => false);
const handleSignInCallback = jest.fn(async () => Promise.resolve());
const getAccessToken = jest.fn(() => {
throw new Error('not authenticated');
});
const injectMock = jest.fn<any, string[]>((): any => {
return undefined;
});

jest.mock('@logto/browser', () => {
return jest.fn().mockImplementation(() => {
return {
isAuthenticated: false,
isSignInRedirected,
handleSignInCallback,
getAccessToken,
signIn: jest.fn(async () => Promise.resolve()),
signOut: jest.fn(async () => Promise.resolve()),
};
});
});

jest.mock('vue', () => {
return {
...jest.requireActual('vue'),
inject: (key: string) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return injectMock(key);
},
};
});

const appId = 'foo';
const endpoint = 'https://endpoint.com';

const appMock = {
provide: jest.fn(),
} as any as App;

describe('createLogto.install', () => {
test('should call LogtoClient constructor and provide Logto context data', async () => {
createLogto.install(appMock, { appId, endpoint });

expect(LogtoClient).toHaveBeenCalledWith({ endpoint, appId });
expect(appMock.provide).toBeCalled();
});
});

describe('Logto plugin not installed', () => {
test('should throw error if calling `useLogto` before install', () => {
expect(() => {
useLogto();
}).toThrowError('Must install Logto plugin first.');
});

test('should throw error if calling `useHandleSignInCallback` before install', () => {
expect(() => {
useHandleSignInCallback();
}).toThrowError('Must install Logto plugin first.');
});
});

describe('useLogto', () => {
beforeEach(() => {
const client = new LogtoClient({ appId, endpoint });
const context = createContext(client);
const { isAuthenticated, isLoading, error } = context;

injectMock.mockImplementationOnce(() => {
return {
isAuthenticated: readonly(isAuthenticated),
isLoading: readonly(isLoading),
error: readonly(error),
...createPluginMethods(context),
};
});
});

test('should inject Logto context data', () => {
const {
isAuthenticated,
isLoading,
error,
signIn,
signOut,
getAccessToken,
getIdTokenClaims,
fetchUserInfo,
} = useLogto();

expect(isAuthenticated.value).toBe(false);
expect(isLoading.value).toBe(false);
expect(error?.value).toBeUndefined();
expect(signIn).toBeInstanceOf(Function);
expect(signOut).toBeInstanceOf(Function);
expect(getAccessToken).toBeInstanceOf(Function);
expect(getIdTokenClaims).toBeInstanceOf(Function);
expect(fetchUserInfo).toBeInstanceOf(Function);
});

test('should return error when getAccessToken fails', async () => {
const client = new LogtoClient({ appId, endpoint });
const context = createContext(client);
const { getAccessToken } = createPluginMethods(context);
const { error } = context;

await getAccessToken();
expect(error.value).not.toBeUndefined();
expect(error.value?.message).toBe('not authenticated');
});
});

describe('useHandleSignInCallback', () => {
beforeEach(() => {
const client = new LogtoClient({ appId, endpoint });
const context = createContext(client);

injectMock.mockImplementation((key: string) => {
if (key === contextInjectionKey) {
return context;
}

if (key === logtoInjectionKey) {
const { isAuthenticated, isLoading, error } = context;

return {
isAuthenticated: readonly(isAuthenticated),
isLoading: readonly(isLoading),
error: readonly(error),
...createPluginMethods(context),
};
}
});
});

test('not in callback url should not call `handleSignInCallback`', async () => {
const { signIn } = useLogto();
useHandleSignInCallback();

await signIn('https://example.com');
expect(handleSignInCallback).not.toHaveBeenCalled();
});

test('in callback url should call `handleSignInCallback`', async () => {
isSignInRedirected.mockImplementationOnce(() => true);
const { signIn } = useLogto();
useHandleSignInCallback();

await signIn('https://example.com');

expect(handleSignInCallback).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 0c0d9e6

Please sign in to comment.