92 changed files with 418 additions and 6161 deletions
@ -0,0 +1,4 @@ |
|||||
|
VITE_API_URL=https://test.tall.wiki/gateway |
||||
|
VITE_BASE_URL=https://test.tall.wiki |
||||
|
VITE_PUBLIC_PATH=/wuliu |
||||
|
VITE_PROXY_URL=/gateway |
||||
@ -0,0 +1,4 @@ |
|||||
|
VITE_API_URL=https://www.tall.wiki/gateway |
||||
|
VITE_BASE_URL=https://www.tall.wiki |
||||
|
VITE_PUBLIC_PATH=/wuliu |
||||
|
VITE_PROXY_URL=/gateway |
||||
@ -1,9 +0,0 @@ |
|||||
{ |
|
||||
"baseUrl": "http://localhost:3000", |
|
||||
"fixturesFolder": "tests/e2e/fixtures", |
|
||||
"integrationFolder": "tests/e2e/integration", |
|
||||
"pluginsFile": "tests/e2e/plugins/index.ts", |
|
||||
"screenshotsFolder": "tests/e2e/screenshots", |
|
||||
"supportFile": "tests/e2e/support/index.ts", |
|
||||
"videosFolder": "tests/e2e/videos" |
|
||||
} |
|
||||
@ -0,0 +1,11 @@ |
|||||
|
@url=https://test.tall.wiki/gateway/logistics |
||||
|
@type=Content-Type: application/json |
||||
|
|
||||
|
POST {{url}}/warehouse/selAllWarehouse |
||||
|
{{type}} |
||||
|
|
||||
|
{ |
||||
|
"param": { |
||||
|
"parkId": 0 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
import http from 'utils/http'; |
||||
|
import axios from 'axios' |
||||
|
|
||||
|
//
|
||||
|
export const getWarehouses = (param: any) => axios.post('/gateway/logistics/mbps/selAllMbps', { param }); |
||||
|
// export const getWarehouses = (param: any) => axios.get('/api/todos/1', );
|
||||
@ -1,10 +0,0 @@ |
|||||
import { defHttp } from '/@/utils/http/axios'; |
|
||||
import { GetAccountInfoModel } from './model/accountModel'; |
|
||||
|
|
||||
enum Api { |
|
||||
ACCOUNT_INFO = '/warehouse/selAllWarehouse', |
|
||||
} |
|
||||
|
|
||||
// Get personal center-basic settings
|
|
||||
|
|
||||
export const accountInfoApi = () => defHttp.get<GetAccountInfoModel>({ url: Api.ACCOUNT_INFO }); |
|
||||
@ -1,12 +0,0 @@ |
|||||
import { defHttp } from '/@/utils/http/axios'; |
|
||||
|
|
||||
enum Api { |
|
||||
// The address does not exist
|
|
||||
Error = '/error', |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Trigger ajax error |
|
||||
*/ |
|
||||
|
|
||||
export const fireErrorApi = () => defHttp.get({ url: Api.Error }); |
|
||||
@ -1,7 +0,0 @@ |
|||||
export interface GetAccountInfoModel { |
|
||||
email: string; |
|
||||
name: string; |
|
||||
introduction: string; |
|
||||
phone: string; |
|
||||
address: string; |
|
||||
} |
|
||||
@ -1,11 +0,0 @@ |
|||||
import { BasicFetchResult } from '/@/api/model/baseModel'; |
|
||||
|
|
||||
export interface DemoOptionsItem { |
|
||||
label: string; |
|
||||
value: string; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Request list return value |
|
||||
*/ |
|
||||
export type DemoOptionsGetResultModel = BasicFetchResult<DemoOptionsItem[]>; |
|
||||
@ -1,74 +0,0 @@ |
|||||
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel'; |
|
||||
|
|
||||
export type AccountParams = BasicPageParams & { |
|
||||
account?: string; |
|
||||
nickname?: string; |
|
||||
}; |
|
||||
|
|
||||
export type RoleParams = { |
|
||||
roleName?: string; |
|
||||
status?: string; |
|
||||
}; |
|
||||
|
|
||||
export type RolePageParams = BasicPageParams & RoleParams; |
|
||||
|
|
||||
export type DeptParams = { |
|
||||
deptName?: string; |
|
||||
status?: string; |
|
||||
}; |
|
||||
|
|
||||
export type MenuParams = { |
|
||||
menuName?: string; |
|
||||
status?: string; |
|
||||
}; |
|
||||
|
|
||||
export interface AccountListItem { |
|
||||
id: string; |
|
||||
account: string; |
|
||||
email: string; |
|
||||
nickname: string; |
|
||||
role: number; |
|
||||
createTime: string; |
|
||||
remark: string; |
|
||||
status: number; |
|
||||
} |
|
||||
|
|
||||
export interface DeptListItem { |
|
||||
id: string; |
|
||||
orderNo: string; |
|
||||
createTime: string; |
|
||||
remark: string; |
|
||||
status: number; |
|
||||
} |
|
||||
|
|
||||
export interface MenuListItem { |
|
||||
id: string; |
|
||||
orderNo: string; |
|
||||
createTime: string; |
|
||||
status: number; |
|
||||
icon: string; |
|
||||
component: string; |
|
||||
permission: string; |
|
||||
} |
|
||||
|
|
||||
export interface RoleListItem { |
|
||||
id: string; |
|
||||
roleName: string; |
|
||||
roleValue: string; |
|
||||
status: number; |
|
||||
orderNo: string; |
|
||||
createTime: string; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Request list return value |
|
||||
*/ |
|
||||
export type AccountListGetResultModel = BasicFetchResult<AccountListItem>; |
|
||||
|
|
||||
export type DeptListGetResultModel = BasicFetchResult<DeptListItem>; |
|
||||
|
|
||||
export type MenuListGetResultModel = BasicFetchResult<MenuListItem>; |
|
||||
|
|
||||
export type RolePageListGetResultModel = BasicFetchResult<RoleListItem>; |
|
||||
|
|
||||
export type RoleListGetResultModel = RoleListItem[]; |
|
||||
@ -1,20 +0,0 @@ |
|||||
import { BasicPageParams, BasicFetchResult } from '/@/api/model/baseModel'; |
|
||||
/** |
|
||||
* @description: Request list interface parameters |
|
||||
*/ |
|
||||
export type DemoParams = BasicPageParams; |
|
||||
|
|
||||
export interface DemoListItem { |
|
||||
id: string; |
|
||||
beginTime: string; |
|
||||
endTime: string; |
|
||||
address: string; |
|
||||
name: string; |
|
||||
no: number; |
|
||||
status: number; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Request list return value |
|
||||
*/ |
|
||||
export type DemoListGetResultModel = BasicFetchResult<DemoListItem>; |
|
||||
@ -1,12 +0,0 @@ |
|||||
import { defHttp } from '/@/utils/http/axios'; |
|
||||
import { DemoOptionsGetResultModel } from './model/optionsModel'; |
|
||||
|
|
||||
enum Api { |
|
||||
OPTIONS_LIST = '/select/getDemoOptions', |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Get sample options value |
|
||||
*/ |
|
||||
export const optionsListApi = () => |
|
||||
defHttp.get<DemoOptionsGetResultModel>({ url: Api.OPTIONS_LIST }); |
|
||||
@ -1,36 +0,0 @@ |
|||||
import { |
|
||||
AccountParams, |
|
||||
DeptListItem, |
|
||||
MenuParams, |
|
||||
RoleParams, |
|
||||
RolePageParams, |
|
||||
MenuListGetResultModel, |
|
||||
DeptListGetResultModel, |
|
||||
AccountListGetResultModel, |
|
||||
RolePageListGetResultModel, |
|
||||
RoleListGetResultModel, |
|
||||
} from './model/systemModel'; |
|
||||
import { defHttp } from '/@/utils/http/axios'; |
|
||||
|
|
||||
enum Api { |
|
||||
AccountList = '/system/getAccountList', |
|
||||
DeptList = '/system/getDeptList', |
|
||||
MenuList = '/system/getMenuList', |
|
||||
RolePageList = '/system/getRoleListByPage', |
|
||||
GetAllRoleList = '/system/getAllRoleList', |
|
||||
} |
|
||||
|
|
||||
export const getAccountList = (params: AccountParams) => |
|
||||
defHttp.get<AccountListGetResultModel>({ url: Api.AccountList, params }); |
|
||||
|
|
||||
export const getDeptList = (params?: DeptListItem) => |
|
||||
defHttp.get<DeptListGetResultModel>({ url: Api.DeptList, params }); |
|
||||
|
|
||||
export const getMenuList = (params?: MenuParams) => |
|
||||
defHttp.get<MenuListGetResultModel>({ url: Api.MenuList, params }); |
|
||||
|
|
||||
export const getRoleListByPage = (params?: RolePageParams) => |
|
||||
defHttp.get<RolePageListGetResultModel>({ url: Api.RolePageList, params }); |
|
||||
|
|
||||
export const getAllRoleList = (params?: RoleParams) => |
|
||||
defHttp.get<RoleListGetResultModel>({ url: Api.GetAllRoleList, params }); |
|
||||
@ -1,19 +0,0 @@ |
|||||
import { defHttp } from '/@/utils/http/axios'; |
|
||||
import { DemoParams, DemoListGetResultModel } from './model/tableModel'; |
|
||||
|
|
||||
enum Api { |
|
||||
DEMO_LIST = '/table/getDemoList', |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Get sample list value |
|
||||
*/ |
|
||||
|
|
||||
export const demoListApi = (params: DemoParams) => |
|
||||
defHttp.get<DemoListGetResultModel>({ |
|
||||
url: Api.DEMO_LIST, |
|
||||
params, |
|
||||
headers: { |
|
||||
ignoreCancelToken: true, |
|
||||
}, |
|
||||
}); |
|
||||
@ -1,9 +0,0 @@ |
|||||
export interface BasicPageParams { |
|
||||
page: number; |
|
||||
pageSize: number; |
|
||||
} |
|
||||
|
|
||||
export interface BasicFetchResult<T extends any> { |
|
||||
items: T; |
|
||||
total: number; |
|
||||
} |
|
||||
@ -1,14 +0,0 @@ |
|||||
import { defHttp } from '/@/utils/http/axios'; |
|
||||
import { getMenuListByIdParams, getMenuListByIdParamsResultModel } from './model/menuModel'; |
|
||||
|
|
||||
enum Api { |
|
||||
GetMenuListById = '/getMenuListById', |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Get user menu based on id |
|
||||
*/ |
|
||||
|
|
||||
export const getMenuListById = (params: getMenuListByIdParams) => { |
|
||||
return defHttp.get<getMenuListByIdParamsResultModel>({ url: Api.GetMenuListById, params }); |
|
||||
}; |
|
||||
@ -1,23 +0,0 @@ |
|||||
import { RouteMeta } from '/@/router/types'; |
|
||||
export interface RouteItem { |
|
||||
path: string; |
|
||||
component: any; |
|
||||
meta: RouteMeta; |
|
||||
name?: string; |
|
||||
alias?: string | string[]; |
|
||||
redirect?: string; |
|
||||
caseSensitive?: boolean; |
|
||||
children?: RouteItem[]; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Get menu interface |
|
||||
*/ |
|
||||
export interface getMenuListByIdParams { |
|
||||
id: number | string; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Get menu return value |
|
||||
*/ |
|
||||
export type getMenuListByIdParamsResultModel = RouteItem[]; |
|
||||
@ -1,5 +0,0 @@ |
|||||
export interface UploadApiResult { |
|
||||
message: string; |
|
||||
code: number; |
|
||||
url: string; |
|
||||
} |
|
||||
@ -1,43 +0,0 @@ |
|||||
/** |
|
||||
* @description: Login interface parameters |
|
||||
*/ |
|
||||
export interface LoginParams { |
|
||||
username: string; |
|
||||
password: string; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Get user information |
|
||||
*/ |
|
||||
export interface GetUserInfoByUserIdParams { |
|
||||
userId: string | number; |
|
||||
} |
|
||||
|
|
||||
export interface RoleInfo { |
|
||||
roleName: string; |
|
||||
value: string; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Login interface return value |
|
||||
*/ |
|
||||
export interface LoginResultModel { |
|
||||
userId: string | number; |
|
||||
token: string; |
|
||||
role: RoleInfo; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Get user information return value |
|
||||
*/ |
|
||||
export interface GetUserInfoByUserIdModel { |
|
||||
roles: RoleInfo[]; |
|
||||
// 用户id
|
|
||||
userId: string | number; |
|
||||
// 用户名
|
|
||||
username: string; |
|
||||
// 真实名字
|
|
||||
realName: string; |
|
||||
// 介绍
|
|
||||
desc?: string; |
|
||||
} |
|
||||
@ -1,22 +0,0 @@ |
|||||
import { UploadApiResult } from './model/uploadModel'; |
|
||||
import { defHttp } from '/@/utils/http/axios'; |
|
||||
import { UploadFileParams } from '/@/utils/http/axios/types'; |
|
||||
import { useGlobSetting } from '/@/hooks/setting'; |
|
||||
|
|
||||
const { uploadUrl = '' } = useGlobSetting(); |
|
||||
|
|
||||
/** |
|
||||
* @description: Upload interface |
|
||||
*/ |
|
||||
export function uploadApi( |
|
||||
params: UploadFileParams, |
|
||||
onUploadProgress: (progressEvent: ProgressEvent) => void |
|
||||
) { |
|
||||
return defHttp.uploadFile<UploadApiResult>( |
|
||||
{ |
|
||||
url: uploadUrl, |
|
||||
onUploadProgress, |
|
||||
}, |
|
||||
params |
|
||||
); |
|
||||
} |
|
||||
@ -1,47 +0,0 @@ |
|||||
import { defHttp } from '/@/utils/http/axios'; |
|
||||
import { |
|
||||
LoginParams, |
|
||||
LoginResultModel, |
|
||||
GetUserInfoByUserIdParams, |
|
||||
GetUserInfoByUserIdModel, |
|
||||
} from './model/userModel'; |
|
||||
|
|
||||
import { ErrorMessageMode } from '/@/utils/http/axios/types'; |
|
||||
|
|
||||
enum Api { |
|
||||
Login = '/login', |
|
||||
GetUserInfoById = '/getUserInfoById', |
|
||||
GetPermCodeByUserId = '/getPermCodeByUserId', |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: user login api |
|
||||
*/ |
|
||||
export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') { |
|
||||
return defHttp.post<LoginResultModel>( |
|
||||
{ |
|
||||
url: Api.Login, |
|
||||
params, |
|
||||
}, |
|
||||
{ |
|
||||
errorMessageMode: mode, |
|
||||
} |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: getUserInfoById |
|
||||
*/ |
|
||||
export function getUserInfoById(params: GetUserInfoByUserIdParams) { |
|
||||
return defHttp.get<GetUserInfoByUserIdModel>({ |
|
||||
url: Api.GetUserInfoById, |
|
||||
params, |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
export function getPermCodeByUserId(params: GetUserInfoByUserIdParams) { |
|
||||
return defHttp.get<string[]>({ |
|
||||
url: Api.GetPermCodeByUserId, |
|
||||
params, |
|
||||
}); |
|
||||
} |
|
||||
@ -1,10 +1,25 @@ |
|||||
import { createApp } from 'vue' |
import { createApp } from 'vue' |
||||
|
import router, { setupRouter } from './router/index'; |
||||
import App from './App.vue' |
import App from './App.vue' |
||||
import './index.css' |
import './index.css' |
||||
import * as echarts from 'echarts' |
import http from 'utils/http'; |
||||
|
|
||||
|
|
||||
const app = createApp(App) |
(async () => { |
||||
app.config.globalProperties.$echarts = echarts |
const app = createApp(App); |
||||
|
|
||||
createApp(App).mount('#app') |
app.config.globalProperties.$http = http; |
||||
|
|
||||
|
// Configure routing
|
||||
|
setupRouter(app); |
||||
|
|
||||
|
// Mount when the route is ready
|
||||
|
// https://next.router.vuejs.org/api/#isready
|
||||
|
await router.isReady(); |
||||
|
|
||||
|
app.mount('#app', true); |
||||
|
|
||||
|
if (import.meta.env.DEV) { |
||||
|
window.__APP__ = app; |
||||
|
} |
||||
|
})(); |
||||
|
|||||
@ -0,0 +1,23 @@ |
|||||
|
import type { RouteRecordRaw } from 'vue-router'; |
||||
|
import type { App } from 'vue'; |
||||
|
|
||||
|
import { createRouter, createWebHashHistory } from 'vue-router'; |
||||
|
|
||||
|
|
||||
|
// app router
|
||||
|
const router = createRouter({ |
||||
|
history: createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH), |
||||
|
routes: [ |
||||
|
{ path: '/', redirect: '/jht' }, |
||||
|
{ path: '/jht', component: () => import('views/repo-five.vue') }, |
||||
|
], |
||||
|
strict: true |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
// config router
|
||||
|
export function setupRouter(app: App<Element>) { |
||||
|
app.use(router); |
||||
|
} |
||||
|
|
||||
|
export default router; |
||||
@ -0,0 +1,98 @@ |
|||||
|
import type { RouteRecordRaw } from 'vue-router'; |
||||
|
import { RoleEnum } from '/@/enums/roleEnum'; |
||||
|
|
||||
|
import { defineComponent } from 'vue'; |
||||
|
|
||||
|
export type Component<T extends any = any> = |
||||
|
| ReturnType<typeof defineComponent> |
||||
|
| (() => Promise<typeof import('*.vue')>) |
||||
|
| (() => Promise<T>); |
||||
|
|
||||
|
export interface RouteMeta { |
||||
|
// title
|
||||
|
title: string; |
||||
|
// Whether to ignore permissions
|
||||
|
ignoreAuth?: boolean; |
||||
|
// role info
|
||||
|
roles?: RoleEnum[]; |
||||
|
// Whether not to cache
|
||||
|
ignoreKeepAlive?: boolean; |
||||
|
// Is it fixed on tab
|
||||
|
affix?: boolean; |
||||
|
// icon on tab
|
||||
|
icon?: string; |
||||
|
|
||||
|
frameSrc?: string; |
||||
|
|
||||
|
// current page transition
|
||||
|
transitionName?: string; |
||||
|
|
||||
|
// Whether the route has been dynamically added
|
||||
|
hideBreadcrumb?: boolean; |
||||
|
|
||||
|
// Hide submenu
|
||||
|
hideChildrenInMenu?: boolean; |
||||
|
|
||||
|
// Carrying parameters
|
||||
|
carryParam?: boolean; |
||||
|
|
||||
|
// Used internally to mark single-level menus
|
||||
|
single?: boolean; |
||||
|
|
||||
|
// Currently active menu
|
||||
|
currentActiveMenu?: string; |
||||
|
|
||||
|
// Never show in tab
|
||||
|
hideTab?: boolean; |
||||
|
|
||||
|
// Never show in menu
|
||||
|
hideMenu?: boolean; |
||||
|
|
||||
|
isLink?: boolean; |
||||
|
} |
||||
|
|
||||
|
// @ts-ignore
|
||||
|
export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> { |
||||
|
name: string; |
||||
|
meta: RouteMeta; |
||||
|
component?: Component | string; |
||||
|
components?: Component; |
||||
|
children?: AppRouteRecordRaw[]; |
||||
|
props?: Recordable; |
||||
|
fullPath?: string; |
||||
|
} |
||||
|
export interface MenuTag { |
||||
|
type?: 'primary' | 'error' | 'warn' | 'success'; |
||||
|
content?: string; |
||||
|
dot?: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface Menu { |
||||
|
name: string; |
||||
|
|
||||
|
icon?: string; |
||||
|
|
||||
|
path: string; |
||||
|
|
||||
|
disabled?: boolean; |
||||
|
|
||||
|
children?: Menu[]; |
||||
|
|
||||
|
orderNo?: number; |
||||
|
|
||||
|
roles?: RoleEnum[]; |
||||
|
|
||||
|
meta?: Partial<RouteMeta>; |
||||
|
|
||||
|
tag?: MenuTag; |
||||
|
|
||||
|
hideMenu?: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface MenuModule { |
||||
|
orderNo?: number; |
||||
|
menu: Menu; |
||||
|
} |
||||
|
|
||||
|
// export type AppRouteModule = RouteModule | AppRouteRecordRaw;
|
||||
|
export type AppRouteModule = AppRouteRecordRaw; |
||||
@ -1,25 +0,0 @@ |
|||||
import { ActionTree, ActionContext } from 'vuex' |
|
||||
|
|
||||
import { State } from './state' |
|
||||
import { Mutations, Mutation } from './mutations' |
|
||||
|
|
||||
export enum Action { |
|
||||
initApp = 'initApp', |
|
||||
} |
|
||||
|
|
||||
type AugmentedActionContext = { |
|
||||
commit<K extends keyof Mutations>( |
|
||||
key: K, |
|
||||
payload?: Parameters<Mutations[K]>[1] |
|
||||
): ReturnType<Mutations[K]> |
|
||||
} & Omit<ActionContext<State, State>, 'commit'> |
|
||||
|
|
||||
export interface Actions { |
|
||||
[Action.initApp]({ state, commit, dispatch }: AugmentedActionContext): void |
|
||||
} |
|
||||
|
|
||||
export const actions: ActionTree<State, State> & Actions = { |
|
||||
[Action.initApp]({ state, commit, dispatch }) { |
|
||||
console.log('app inited!') |
|
||||
}, |
|
||||
} |
|
||||
@ -1,12 +0,0 @@ |
|||||
import { GetterTree } from 'vuex' |
|
||||
import { State } from './state' |
|
||||
|
|
||||
export type Getters = { |
|
||||
isReady(state: State): boolean |
|
||||
} |
|
||||
|
|
||||
export const getters: GetterTree<State, State> & Getters = { |
|
||||
isReady(state) { |
|
||||
return !state.isInitialized |
|
||||
}, |
|
||||
} |
|
||||
@ -1,42 +0,0 @@ |
|||||
/* eslint-disable @typescript-eslint/no-extra-semi */ |
|
||||
import { InjectionKey } from 'vue' |
|
||||
import { createStore, Store as VuexStore, CommitOptions, DispatchOptions } from 'vuex' |
|
||||
|
|
||||
import { mutations, Mutations, Mutation } from './mutations' |
|
||||
import { actions, Actions, Action } from './actions' |
|
||||
import { getters, Getters } from './getters' |
|
||||
import { state } from './state' |
|
||||
import type { State } from './state' |
|
||||
|
|
||||
export const key: InjectionKey<VuexStore<State>> = Symbol() |
|
||||
|
|
||||
export const store = createStore({ |
|
||||
state, |
|
||||
getters, |
|
||||
mutations, |
|
||||
actions, |
|
||||
}) |
|
||||
|
|
||||
export type Store = Omit<VuexStore<State>, 'getters' | 'commit' | 'dispatch'> & { |
|
||||
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>( |
|
||||
key: K, |
|
||||
payload: P, |
|
||||
options?: CommitOptions |
|
||||
): ReturnType<Mutations[K]> |
|
||||
} & { |
|
||||
dispatch<K extends keyof Actions>( |
|
||||
key: K, |
|
||||
payload?: Parameters<Actions[K]>[1], |
|
||||
options?: DispatchOptions |
|
||||
): ReturnType<Actions[K]> |
|
||||
} & { |
|
||||
getters: { |
|
||||
[K in keyof Getters]: ReturnType<Getters[K]> |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function useStore(): Store { |
|
||||
return store as Store |
|
||||
} |
|
||||
|
|
||||
export { State, Mutation, Action } |
|
||||
@ -1,16 +0,0 @@ |
|||||
import { MutationTree } from 'vuex' |
|
||||
import { State } from './state' |
|
||||
|
|
||||
export enum Mutation { |
|
||||
INCREMENT = 'INCREMENT', |
|
||||
} |
|
||||
|
|
||||
export type Mutations<S = State> = { |
|
||||
[Mutation.INCREMENT](state: S, payload: number): void |
|
||||
} |
|
||||
|
|
||||
export const mutations: MutationTree<State> & Mutations = { |
|
||||
[Mutation.INCREMENT](state: State, payload: number = 1) { |
|
||||
state.count += payload |
|
||||
}, |
|
||||
} |
|
||||
@ -1,15 +0,0 @@ |
|||||
export interface State { |
|
||||
debug: boolean |
|
||||
version: string |
|
||||
isInitialized: boolean |
|
||||
count: number |
|
||||
} |
|
||||
|
|
||||
const versionString = import.meta.env.MODE === 'development' ? _APP_VERSION + '-dev' : _APP_VERSION |
|
||||
|
|
||||
export const state: State = { |
|
||||
debug: import.meta.env.MODE === 'development', |
|
||||
version: versionString, |
|
||||
isInitialized: false, |
|
||||
count: 0, |
|
||||
} |
|
||||
@ -1,21 +0,0 @@ |
|||||
import { Persistent, BasicKeys } from '/@/utils/cache/persistent'; |
|
||||
import { CacheTypeEnum } from '/@/enums/cacheEnum'; |
|
||||
import projectSetting from '/@/settings/projectSetting'; |
|
||||
import { TOKEN_KEY } from '/@/enums/cacheEnum'; |
|
||||
|
|
||||
const { permissionCacheType } = projectSetting; |
|
||||
const isLocal = permissionCacheType === CacheTypeEnum.LOCAL; |
|
||||
|
|
||||
export function getToken() { |
|
||||
return getAuthCache(TOKEN_KEY); |
|
||||
} |
|
||||
|
|
||||
export function getAuthCache<T>(key: BasicKeys) { |
|
||||
const fn = isLocal ? Persistent.getLocal : Persistent.getSession; |
|
||||
return fn(key) as T; |
|
||||
} |
|
||||
|
|
||||
export function setAuthCache(key: BasicKeys, value) { |
|
||||
const fn = isLocal ? Persistent.setLocal : Persistent.setSession; |
|
||||
return fn(key, value, true); |
|
||||
} |
|
||||
@ -1,32 +0,0 @@ |
|||||
import { getStorageShortName } from '/@/utils/env'; |
|
||||
import { createStorage as create, CreateStorageParams } from './storageCache'; |
|
||||
import { enableStorageEncryption } from '/@/settings/encryptionSetting'; |
|
||||
import { DEFAULT_CACHE_TIME } from '/@/settings/encryptionSetting'; |
|
||||
|
|
||||
export type Options = Partial<CreateStorageParams>; |
|
||||
|
|
||||
const createOptions = (storage: Storage, options: Options = {}): Options => { |
|
||||
return { |
|
||||
// No encryption in debug mode
|
|
||||
hasEncrypt: enableStorageEncryption, |
|
||||
storage, |
|
||||
prefixKey: getStorageShortName(), |
|
||||
...options, |
|
||||
}; |
|
||||
}; |
|
||||
|
|
||||
export const WebStorage = create(createOptions(sessionStorage)); |
|
||||
|
|
||||
export const createStorage = (storage: Storage = sessionStorage, options: Options = {}) => { |
|
||||
return create(createOptions(storage, options)); |
|
||||
}; |
|
||||
|
|
||||
export const createSessionStorage = (options: Options = {}) => { |
|
||||
return createStorage(sessionStorage, { ...options, timeout: DEFAULT_CACHE_TIME }); |
|
||||
}; |
|
||||
|
|
||||
export const createLocalStorage = (options: Options = {}) => { |
|
||||
return createStorage(localStorage, { ...options, timeout: DEFAULT_CACHE_TIME }); |
|
||||
}; |
|
||||
|
|
||||
export default WebStorage; |
|
||||
@ -1,102 +0,0 @@ |
|||||
export interface Cache<V = any> { |
|
||||
value?: V; |
|
||||
timeoutId?: ReturnType<typeof setTimeout>; |
|
||||
time?: number; |
|
||||
alive?: number; |
|
||||
} |
|
||||
|
|
||||
const NOT_ALIVE = 0; |
|
||||
|
|
||||
export class Memory<T = any, V = any> { |
|
||||
private cache: { [key in keyof T]?: Cache<V> } = {}; |
|
||||
private alive: number; |
|
||||
|
|
||||
constructor(alive = NOT_ALIVE) { |
|
||||
// Unit second
|
|
||||
this.alive = alive * 1000; |
|
||||
} |
|
||||
|
|
||||
get getCache() { |
|
||||
return this.cache; |
|
||||
} |
|
||||
|
|
||||
setCache(cache) { |
|
||||
this.cache = cache; |
|
||||
} |
|
||||
|
|
||||
// get<K extends keyof T>(key: K) {
|
|
||||
// const item = this.getItem(key);
|
|
||||
// const time = item?.time;
|
|
||||
// if (!isNullOrUnDef(time) && time < new Date().getTime()) {
|
|
||||
// this.remove(key);
|
|
||||
// }
|
|
||||
// return item?.value ?? undefined;
|
|
||||
// }
|
|
||||
|
|
||||
get<K extends keyof T>(key: K) { |
|
||||
return this.cache[key]; |
|
||||
} |
|
||||
|
|
||||
set<K extends keyof T>(key: K, value: V, expires?: number) { |
|
||||
let item = this.get(key); |
|
||||
|
|
||||
if (!expires || (expires as number) <= 0) { |
|
||||
expires = this.alive; |
|
||||
} |
|
||||
if (item) { |
|
||||
if (item.timeoutId) { |
|
||||
clearTimeout(item.timeoutId); |
|
||||
item.timeoutId = undefined; |
|
||||
} |
|
||||
item.value = value; |
|
||||
} else { |
|
||||
item = { value, alive: expires }; |
|
||||
this.cache[key] = item; |
|
||||
} |
|
||||
|
|
||||
if (!expires) { |
|
||||
return value; |
|
||||
} |
|
||||
const now = new Date().getTime(); |
|
||||
item.time = now + this.alive; |
|
||||
item.timeoutId = setTimeout( |
|
||||
() => { |
|
||||
this.remove(key); |
|
||||
}, |
|
||||
expires > now ? expires - now : expires |
|
||||
); |
|
||||
|
|
||||
return value; |
|
||||
} |
|
||||
|
|
||||
remove<K extends keyof T>(key: K) { |
|
||||
const item = this.get(key); |
|
||||
Reflect.deleteProperty(this.cache, key); |
|
||||
if (item) { |
|
||||
clearTimeout(item.timeoutId!); |
|
||||
return item.value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
resetCache(cache: { [K in keyof T]: Cache }) { |
|
||||
Object.keys(cache).forEach((key) => { |
|
||||
const k = (key as any) as keyof T; |
|
||||
const item = cache[k]; |
|
||||
if (item && item.time) { |
|
||||
const now = new Date().getTime(); |
|
||||
const expire = item.time; |
|
||||
if (expire > now) { |
|
||||
this.set(k, item.value, expire); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
clear() { |
|
||||
Object.keys(this.cache).forEach((key) => { |
|
||||
const item = this.cache[key]; |
|
||||
item.timeoutId && clearTimeout(item.timeoutId); |
|
||||
}); |
|
||||
this.cache = {}; |
|
||||
} |
|
||||
} |
|
||||
@ -1,115 +0,0 @@ |
|||||
import type { LockInfo, UserInfo } from '/#/store'; |
|
||||
import type { ProjectConfig } from '/#/config'; |
|
||||
import type { RouteLocationNormalized } from 'vue-router'; |
|
||||
|
|
||||
import { createLocalStorage, createSessionStorage } from '/@/utils/cache'; |
|
||||
import { Memory } from './memory'; |
|
||||
import { |
|
||||
TOKEN_KEY, |
|
||||
USER_INFO_KEY, |
|
||||
ROLES_KEY, |
|
||||
LOCK_INFO_KEY, |
|
||||
PROJ_CFG_KEY, |
|
||||
APP_LOCAL_CACHE_KEY, |
|
||||
APP_SESSION_CACHE_KEY, |
|
||||
MULTIPLE_TABS_KEY, |
|
||||
} from '/@/enums/cacheEnum'; |
|
||||
import { DEFAULT_CACHE_TIME } from '/@/settings/encryptionSetting'; |
|
||||
import { toRaw } from 'vue'; |
|
||||
|
|
||||
interface BasicStore { |
|
||||
[TOKEN_KEY]: string | number | null | undefined; |
|
||||
[USER_INFO_KEY]: UserInfo; |
|
||||
[ROLES_KEY]: string[]; |
|
||||
[LOCK_INFO_KEY]: LockInfo; |
|
||||
[PROJ_CFG_KEY]: ProjectConfig; |
|
||||
[MULTIPLE_TABS_KEY]: RouteLocationNormalized[]; |
|
||||
} |
|
||||
|
|
||||
type LocalStore = BasicStore; |
|
||||
|
|
||||
type SessionStore = BasicStore; |
|
||||
|
|
||||
export type BasicKeys = keyof BasicStore; |
|
||||
type LocalKeys = keyof LocalStore; |
|
||||
type SessionKeys = keyof SessionStore; |
|
||||
|
|
||||
const ls = createLocalStorage(); |
|
||||
const ss = createSessionStorage(); |
|
||||
|
|
||||
const localMemory = new Memory(DEFAULT_CACHE_TIME); |
|
||||
const sessionMemory = new Memory(DEFAULT_CACHE_TIME); |
|
||||
|
|
||||
function initPersistentMemory() { |
|
||||
const localCache = ls.get(APP_LOCAL_CACHE_KEY); |
|
||||
const sessionCache = ss.get(APP_SESSION_CACHE_KEY); |
|
||||
localCache && localMemory.resetCache(localCache); |
|
||||
sessionCache && sessionMemory.resetCache(sessionCache); |
|
||||
} |
|
||||
|
|
||||
export class Persistent { |
|
||||
static getLocal<T>(key: LocalKeys) { |
|
||||
return localMemory.get(key)?.value as Nullable<T>; |
|
||||
} |
|
||||
|
|
||||
static setLocal(key: LocalKeys, value: LocalStore[LocalKeys], immediate = false): void { |
|
||||
localMemory.set(key, toRaw(value)); |
|
||||
immediate && ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache); |
|
||||
} |
|
||||
|
|
||||
static removeLocal(key: LocalKeys): void { |
|
||||
localMemory.remove(key); |
|
||||
} |
|
||||
|
|
||||
static clearLocal(): void { |
|
||||
localMemory.clear(); |
|
||||
} |
|
||||
|
|
||||
static getSession<T>(key: SessionKeys) { |
|
||||
return sessionMemory.get(key)?.value as Nullable<T>; |
|
||||
} |
|
||||
|
|
||||
static setSession(key: SessionKeys, value: SessionStore[SessionKeys], immediate = false): void { |
|
||||
sessionMemory.set(key, toRaw(value)); |
|
||||
immediate && ss.set(APP_SESSION_CACHE_KEY, sessionMemory.getCache); |
|
||||
} |
|
||||
|
|
||||
static removeSession(key: SessionKeys): void { |
|
||||
sessionMemory.remove(key); |
|
||||
} |
|
||||
static clearSession(): void { |
|
||||
sessionMemory.clear(); |
|
||||
} |
|
||||
|
|
||||
static clearAll() { |
|
||||
sessionMemory.clear(); |
|
||||
localMemory.clear(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
window.addEventListener('beforeunload', function () { |
|
||||
ls.set(APP_LOCAL_CACHE_KEY, localMemory.getCache); |
|
||||
ss.set(APP_SESSION_CACHE_KEY, sessionMemory.getCache); |
|
||||
}); |
|
||||
|
|
||||
function storageChange(e: any) { |
|
||||
const { key, newValue, oldValue } = e; |
|
||||
|
|
||||
if (!key) { |
|
||||
Persistent.clearAll(); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (!!newValue && !!oldValue) { |
|
||||
if (APP_LOCAL_CACHE_KEY === key) { |
|
||||
Persistent.clearLocal(); |
|
||||
} |
|
||||
if (APP_SESSION_CACHE_KEY === key) { |
|
||||
Persistent.clearSession(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
window.addEventListener('storage', storageChange); |
|
||||
|
|
||||
initPersistentMemory(); |
|
||||
@ -1,114 +0,0 @@ |
|||||
import { cacheCipher } from '/@/settings/encryptionSetting'; |
|
||||
|
|
||||
import type { EncryptionParams } from '/@/utils/cipher'; |
|
||||
|
|
||||
import { AesEncryption } from '/@/utils/cipher'; |
|
||||
|
|
||||
import { isNullOrUnDef } from '/@/utils/is'; |
|
||||
|
|
||||
export interface CreateStorageParams extends EncryptionParams { |
|
||||
prefixKey: string; |
|
||||
storage: Storage; |
|
||||
hasEncrypt: boolean; |
|
||||
timeout?: Nullable<number>; |
|
||||
} |
|
||||
export const createStorage = ({ |
|
||||
prefixKey = '', |
|
||||
storage = sessionStorage, |
|
||||
key = cacheCipher.key, |
|
||||
iv = cacheCipher.iv, |
|
||||
timeout = null, |
|
||||
hasEncrypt = true, |
|
||||
}: Partial<CreateStorageParams> = {}) => { |
|
||||
if (hasEncrypt && [key.length, iv.length].some((item) => item !== 16)) { |
|
||||
throw new Error('When hasEncrypt is true, the key or iv must be 16 bits!'); |
|
||||
} |
|
||||
|
|
||||
const encryption = new AesEncryption({ key, iv }); |
|
||||
|
|
||||
/** |
|
||||
*Cache class |
|
||||
*Construction parameters can be passed into sessionStorage, localStorage, |
|
||||
* @class Cache |
|
||||
* @example |
|
||||
*/ |
|
||||
const WebStorage = class WebStorage { |
|
||||
private storage: Storage; |
|
||||
private prefixKey?: string; |
|
||||
private encryption: AesEncryption; |
|
||||
private hasEncrypt: boolean; |
|
||||
/** |
|
||||
* |
|
||||
* @param {*} storage |
|
||||
*/ |
|
||||
constructor() { |
|
||||
this.storage = storage; |
|
||||
this.prefixKey = prefixKey; |
|
||||
this.encryption = encryption; |
|
||||
this.hasEncrypt = hasEncrypt; |
|
||||
} |
|
||||
|
|
||||
private getKey(key: string) { |
|
||||
return `${this.prefixKey}${key}`.toUpperCase(); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* |
|
||||
* Set cache |
|
||||
* @param {string} key |
|
||||
* @param {*} value |
|
||||
* @expire Expiration time in seconds |
|
||||
* @memberof Cache |
|
||||
*/ |
|
||||
set(key: string, value: any, expire: number | null = timeout) { |
|
||||
const stringData = JSON.stringify({ |
|
||||
value, |
|
||||
time: Date.now(), |
|
||||
expire: !isNullOrUnDef(expire) ? new Date().getTime() + expire * 1000 : null, |
|
||||
}); |
|
||||
const stringifyValue = this.hasEncrypt |
|
||||
? this.encryption.encryptByAES(stringData) |
|
||||
: stringData; |
|
||||
this.storage.setItem(this.getKey(key), stringifyValue); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
*Read cache |
|
||||
* @param {string} key |
|
||||
* @memberof Cache |
|
||||
*/ |
|
||||
get(key: string, def: any = null): any { |
|
||||
const val = this.storage.getItem(this.getKey(key)); |
|
||||
if (!val) return def; |
|
||||
|
|
||||
try { |
|
||||
const decVal = this.hasEncrypt ? this.encryption.decryptByAES(val) : val; |
|
||||
const data = JSON.parse(decVal); |
|
||||
const { value, expire } = data; |
|
||||
if (isNullOrUnDef(expire) || expire >= new Date().getTime()) { |
|
||||
return value; |
|
||||
} |
|
||||
this.remove(key); |
|
||||
} catch (e) { |
|
||||
return def; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Delete cache based on key |
|
||||
* @param {string} key |
|
||||
* @memberof Cache |
|
||||
*/ |
|
||||
remove(key: string) { |
|
||||
this.storage.removeItem(this.getKey(key)); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Delete all caches of this instance |
|
||||
*/ |
|
||||
clear(): void { |
|
||||
this.storage.clear(); |
|
||||
} |
|
||||
}; |
|
||||
return new WebStorage(); |
|
||||
}; |
|
||||
@ -1,55 +0,0 @@ |
|||||
import { encrypt, decrypt } from 'crypto-js/aes'; |
|
||||
import { parse } from 'crypto-js/enc-utf8'; |
|
||||
import pkcs7 from 'crypto-js/pad-pkcs7'; |
|
||||
import ECB from 'crypto-js/mode-ecb'; |
|
||||
import md5 from 'crypto-js/md5'; |
|
||||
import UTF8 from 'crypto-js/enc-utf8'; |
|
||||
import Base64 from 'crypto-js/enc-base64'; |
|
||||
|
|
||||
export interface EncryptionParams { |
|
||||
key: string; |
|
||||
iv: string; |
|
||||
} |
|
||||
|
|
||||
export class AesEncryption { |
|
||||
private key; |
|
||||
private iv; |
|
||||
|
|
||||
constructor(opt: Partial<EncryptionParams> = {}) { |
|
||||
const { key, iv } = opt; |
|
||||
if (key) { |
|
||||
this.key = parse(key); |
|
||||
} |
|
||||
if (iv) { |
|
||||
this.iv = parse(iv); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
get getOptions() { |
|
||||
return { |
|
||||
mode: ECB, |
|
||||
padding: pkcs7, |
|
||||
iv: this.iv, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
encryptByAES(cipherText: string) { |
|
||||
return encrypt(cipherText, this.key, this.getOptions).toString(); |
|
||||
} |
|
||||
|
|
||||
decryptByAES(cipherText: string) { |
|
||||
return decrypt(cipherText, this.key, this.getOptions).toString(UTF8); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function encryptByBase64(cipherText: string) { |
|
||||
return Base64.parse(cipherText).toString(UTF8); |
|
||||
} |
|
||||
|
|
||||
export function decodeByBase64(cipherText: string) { |
|
||||
return Base64.parse(cipherText).toString(UTF8); |
|
||||
} |
|
||||
|
|
||||
export function encryptByMd5(password: string) { |
|
||||
return md5(password).toString(); |
|
||||
} |
|
||||
@ -1,151 +0,0 @@ |
|||||
/** |
|
||||
* 判断是否 十六进制颜色值. |
|
||||
* 输入形式可为 #fff000 #f00 |
|
||||
* |
|
||||
* @param String color 十六进制颜色值 |
|
||||
* @return Boolean |
|
||||
*/ |
|
||||
export function isHexColor(color: string) { |
|
||||
const reg = /^#([0-9a-fA-F]{3}|[0-9a-fA-f]{6})$/; |
|
||||
return reg.test(color); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* RGB 颜色值转换为 十六进制颜色值. |
|
||||
* r, g, 和 b 需要在 [0, 255] 范围内 |
|
||||
* |
|
||||
* @return String 类似#ff00ff |
|
||||
* @param r |
|
||||
* @param g |
|
||||
* @param b |
|
||||
*/ |
|
||||
export function rgbToHex(r: number, g: number, b: number) { |
|
||||
// tslint:disable-next-line:no-bitwise
|
|
||||
const hex = ((r << 16) | (g << 8) | b).toString(16); |
|
||||
return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Transform a HEX color to its RGB representation |
|
||||
* @param {string} hex The color to transform |
|
||||
* @returns The RGB representation of the passed color |
|
||||
*/ |
|
||||
export function hexToRGB(hex: string) { |
|
||||
let sHex = hex.toLowerCase(); |
|
||||
if (isHexColor(hex)) { |
|
||||
if (sHex.length === 4) { |
|
||||
let sColorNew = '#'; |
|
||||
for (let i = 1; i < 4; i += 1) { |
|
||||
sColorNew += sHex.slice(i, i + 1).concat(sHex.slice(i, i + 1)); |
|
||||
} |
|
||||
sHex = sColorNew; |
|
||||
} |
|
||||
const sColorChange: number[] = []; |
|
||||
for (let i = 1; i < 7; i += 2) { |
|
||||
sColorChange.push(parseInt('0x' + sHex.slice(i, i + 2))); |
|
||||
} |
|
||||
return 'RGB(' + sColorChange.join(',') + ')'; |
|
||||
} |
|
||||
return sHex; |
|
||||
} |
|
||||
|
|
||||
export function colorIsDark(color: string) { |
|
||||
if (!isHexColor(color)) return; |
|
||||
const [r, g, b] = hexToRGB(color) |
|
||||
.replace(/(?:\(|\)|rgb|RGB)*/g, '') |
|
||||
.split(',') |
|
||||
.map((item) => Number(item)); |
|
||||
return r * 0.299 + g * 0.578 + b * 0.114 < 192; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Darkens a HEX color given the passed percentage |
|
||||
* @param {string} color The color to process |
|
||||
* @param {number} amount The amount to change the color by |
|
||||
* @returns {string} The HEX representation of the processed color |
|
||||
*/ |
|
||||
export function darken(color: string, amount: number) { |
|
||||
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color; |
|
||||
amount = Math.trunc((255 * amount) / 100); |
|
||||
return `#${subtractLight(color.substring(0, 2), amount)}${subtractLight( |
|
||||
color.substring(2, 4), |
|
||||
amount |
|
||||
)}${subtractLight(color.substring(4, 6), amount)}`;
|
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Lightens a 6 char HEX color according to the passed percentage |
|
||||
* @param {string} color The color to change |
|
||||
* @param {number} amount The amount to change the color by |
|
||||
* @returns {string} The processed color represented as HEX |
|
||||
*/ |
|
||||
export function lighten(color: string, amount: number) { |
|
||||
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color; |
|
||||
amount = Math.trunc((255 * amount) / 100); |
|
||||
return `#${addLight(color.substring(0, 2), amount)}${addLight( |
|
||||
color.substring(2, 4), |
|
||||
amount |
|
||||
)}${addLight(color.substring(4, 6), amount)}`;
|
|
||||
} |
|
||||
|
|
||||
/* Suma el porcentaje indicado a un color (RR, GG o BB) hexadecimal para aclararlo */ |
|
||||
/** |
|
||||
* Sums the passed percentage to the R, G or B of a HEX color |
|
||||
* @param {string} color The color to change |
|
||||
* @param {number} amount The amount to change the color by |
|
||||
* @returns {string} The processed part of the color |
|
||||
*/ |
|
||||
function addLight(color: string, amount: number) { |
|
||||
const cc = parseInt(color, 16) + amount; |
|
||||
const c = cc > 255 ? 255 : cc; |
|
||||
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Calculates luminance of an rgb color |
|
||||
* @param {number} r red |
|
||||
* @param {number} g green |
|
||||
* @param {number} b blue |
|
||||
*/ |
|
||||
function luminanace(r: number, g: number, b: number) { |
|
||||
const a = [r, g, b].map((v) => { |
|
||||
v /= 255; |
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); |
|
||||
}); |
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Calculates contrast between two rgb colors |
|
||||
* @param {string} rgb1 rgb color 1 |
|
||||
* @param {string} rgb2 rgb color 2 |
|
||||
*/ |
|
||||
function contrast(rgb1: string[], rgb2: number[]) { |
|
||||
return ( |
|
||||
(luminanace(~~rgb1[0], ~~rgb1[1], ~~rgb1[2]) + 0.05) / |
|
||||
(luminanace(rgb2[0], rgb2[1], rgb2[2]) + 0.05) |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Determines what the best text color is (black or white) based con the contrast with the background |
|
||||
* @param hexColor - Last selected color by the user |
|
||||
*/ |
|
||||
export function calculateBestTextColor(hexColor: string) { |
|
||||
const rgbColor = hexToRGB(hexColor.substring(1)); |
|
||||
const contrastWithBlack = contrast(rgbColor.split(','), [0, 0, 0]); |
|
||||
|
|
||||
return contrastWithBlack >= 12 ? '#000000' : '#FFFFFF'; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Subtracts the indicated percentage to the R, G or B of a HEX color |
|
||||
* @param {string} color The color to change |
|
||||
* @param {number} amount The amount to change the color by |
|
||||
* @returns {string} The processed part of the color |
|
||||
*/ |
|
||||
function subtractLight(color: string, amount: number) { |
|
||||
const cc = parseInt(color, 16) - amount; |
|
||||
const c = cc < 0 ? 0 : cc; |
|
||||
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`; |
|
||||
} |
|
||||
@ -1,20 +0,0 @@ |
|||||
/** |
|
||||
* Independent time operation tool to facilitate subsequent switch to dayjs |
|
||||
*/ |
|
||||
import moment from 'moment'; |
|
||||
|
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'; |
|
||||
const DATE_FORMAT = 'YYYY-MM-DD '; |
|
||||
|
|
||||
export function formatToDateTime( |
|
||||
date: moment.MomentInput = null, |
|
||||
format = DATE_TIME_FORMAT |
|
||||
): string { |
|
||||
return moment(date).format(format); |
|
||||
} |
|
||||
|
|
||||
export function formatToDate(date: moment.MomentInput = null, format = DATE_FORMAT): string { |
|
||||
return moment(date).format(format); |
|
||||
} |
|
||||
|
|
||||
export const dateUtil = moment; |
|
||||
@ -1,165 +0,0 @@ |
|||||
import { upperFirst } from 'lodash-es'; |
|
||||
|
|
||||
export interface ViewportOffsetResult { |
|
||||
left: number; |
|
||||
top: number; |
|
||||
right: number; |
|
||||
bottom: number; |
|
||||
rightIncludeBody: number; |
|
||||
bottomIncludeBody: number; |
|
||||
} |
|
||||
|
|
||||
export function getBoundingClientRect(element: Element): DOMRect | number { |
|
||||
if (!element || !element.getBoundingClientRect) { |
|
||||
return 0; |
|
||||
} |
|
||||
return element.getBoundingClientRect(); |
|
||||
} |
|
||||
|
|
||||
function trim(string: string) { |
|
||||
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, ''); |
|
||||
} |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
export function hasClass(el: Element, cls: string) { |
|
||||
if (!el || !cls) return false; |
|
||||
if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.'); |
|
||||
if (el.classList) { |
|
||||
return el.classList.contains(cls); |
|
||||
} else { |
|
||||
return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
export function addClass(el: Element, cls: string) { |
|
||||
if (!el) return; |
|
||||
let curClass = el.className; |
|
||||
const classes = (cls || '').split(' '); |
|
||||
|
|
||||
for (let i = 0, j = classes.length; i < j; i++) { |
|
||||
const clsName = classes[i]; |
|
||||
if (!clsName) continue; |
|
||||
|
|
||||
if (el.classList) { |
|
||||
el.classList.add(clsName); |
|
||||
} else if (!hasClass(el, clsName)) { |
|
||||
curClass += ' ' + clsName; |
|
||||
} |
|
||||
} |
|
||||
if (!el.classList) { |
|
||||
el.className = curClass; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
export function removeClass(el: Element, cls: string) { |
|
||||
if (!el || !cls) return; |
|
||||
const classes = cls.split(' '); |
|
||||
let curClass = ' ' + el.className + ' '; |
|
||||
|
|
||||
for (let i = 0, j = classes.length; i < j; i++) { |
|
||||
const clsName = classes[i]; |
|
||||
if (!clsName) continue; |
|
||||
|
|
||||
if (el.classList) { |
|
||||
el.classList.remove(clsName); |
|
||||
} else if (hasClass(el, clsName)) { |
|
||||
curClass = curClass.replace(' ' + clsName + ' ', ' '); |
|
||||
} |
|
||||
} |
|
||||
if (!el.classList) { |
|
||||
el.className = trim(curClass); |
|
||||
} |
|
||||
} |
|
||||
/** |
|
||||
* Get the left and top offset of the current element |
|
||||
* left: the distance between the leftmost element and the left side of the document |
|
||||
* top: the distance from the top of the element to the top of the document |
|
||||
* right: the distance from the far right of the element to the right of the document |
|
||||
* bottom: the distance from the bottom of the element to the bottom of the document |
|
||||
* rightIncludeBody: the distance between the leftmost element and the right side of the document |
|
||||
* bottomIncludeBody: the distance from the bottom of the element to the bottom of the document |
|
||||
* |
|
||||
* @description: |
|
||||
*/ |
|
||||
export function getViewportOffset(element: Element): ViewportOffsetResult { |
|
||||
const doc = document.documentElement; |
|
||||
|
|
||||
const docScrollLeft = doc.scrollLeft; |
|
||||
const docScrollTop = doc.scrollTop; |
|
||||
const docClientLeft = doc.clientLeft; |
|
||||
const docClientTop = doc.clientTop; |
|
||||
|
|
||||
const pageXOffset = window.pageXOffset; |
|
||||
const pageYOffset = window.pageYOffset; |
|
||||
|
|
||||
const box = getBoundingClientRect(element); |
|
||||
|
|
||||
const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect; |
|
||||
|
|
||||
const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0); |
|
||||
const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0); |
|
||||
const offsetLeft = retLeft + pageXOffset; |
|
||||
const offsetTop = rectTop + pageYOffset; |
|
||||
|
|
||||
const left = offsetLeft - scrollLeft; |
|
||||
const top = offsetTop - scrollTop; |
|
||||
|
|
||||
const clientWidth = window.document.documentElement.clientWidth; |
|
||||
const clientHeight = window.document.documentElement.clientHeight; |
|
||||
return { |
|
||||
left: left, |
|
||||
top: top, |
|
||||
right: clientWidth - rectWidth - left, |
|
||||
bottom: clientHeight - rectHeight - top, |
|
||||
rightIncludeBody: clientWidth - left, |
|
||||
bottomIncludeBody: clientHeight - top, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
export function hackCss(attr: string, value: string) { |
|
||||
const prefix: string[] = ['webkit', 'Moz', 'ms', 'OT']; |
|
||||
|
|
||||
const styleObj: any = {}; |
|
||||
prefix.forEach((item) => { |
|
||||
styleObj[`${item}${upperFirst(attr)}`] = value; |
|
||||
}); |
|
||||
return { |
|
||||
...styleObj, |
|
||||
[attr]: value, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
export function on( |
|
||||
element: Element | HTMLElement | Document | Window, |
|
||||
event: string, |
|
||||
handler: EventListenerOrEventListenerObject |
|
||||
): void { |
|
||||
if (element && event && handler) { |
|
||||
element.addEventListener(event, handler, false); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
export function off( |
|
||||
element: Element | HTMLElement | Document | Window, |
|
||||
event: string, |
|
||||
handler: Fn |
|
||||
): void { |
|
||||
if (element && event && handler) { |
|
||||
element.removeEventListener(event, handler, false); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
export function once(el: HTMLElement, event: string, fn: EventListener): void { |
|
||||
const listener = function (this: any, ...args: unknown[]) { |
|
||||
if (fn) { |
|
||||
fn.apply(this, args); |
|
||||
} |
|
||||
off(el, event, listener); |
|
||||
}; |
|
||||
on(el, event, listener); |
|
||||
} |
|
||||
@ -1,83 +0,0 @@ |
|||||
import type { GlobEnvConfig } from '/#/config'; |
|
||||
|
|
||||
import { warn } from '/@/utils/log'; |
|
||||
import pkg from '../../package.json'; |
|
||||
import { getConfigFileName } from '../../build/getConfigFileName'; |
|
||||
|
|
||||
export function getCommonStoragePrefix() { |
|
||||
const { VITE_GLOB_APP_SHORT_NAME } = getAppEnvConfig(); |
|
||||
return `${VITE_GLOB_APP_SHORT_NAME}__${getEnv()}`.toUpperCase(); |
|
||||
} |
|
||||
|
|
||||
// Generate cache key according to version
|
|
||||
export function getStorageShortName() { |
|
||||
return `${getCommonStoragePrefix()}${`__${pkg.version}`}__`.toUpperCase(); |
|
||||
} |
|
||||
|
|
||||
export function getAppEnvConfig() { |
|
||||
const ENV_NAME = getConfigFileName(import.meta.env); |
|
||||
|
|
||||
const ENV = ((import.meta.env.DEV |
|
||||
? // Get the global configuration (the configuration will be extracted independently when packaging)
|
|
||||
((import.meta.env as unknown) as GlobEnvConfig) |
|
||||
: window[ENV_NAME as any]) as unknown) as GlobEnvConfig; |
|
||||
|
|
||||
const { |
|
||||
VITE_GLOB_APP_TITLE, |
|
||||
VITE_GLOB_API_URL, |
|
||||
VITE_GLOB_APP_SHORT_NAME, |
|
||||
VITE_GLOB_API_URL_PREFIX, |
|
||||
VITE_GLOB_UPLOAD_URL, |
|
||||
} = ENV; |
|
||||
|
|
||||
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) { |
|
||||
warn( |
|
||||
`VITE_GLOB_APP_SHORT_NAME Variables can only be characters/underscores, please modify in the environment variables and re-running.` |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
VITE_GLOB_APP_TITLE, |
|
||||
VITE_GLOB_API_URL, |
|
||||
VITE_GLOB_APP_SHORT_NAME, |
|
||||
VITE_GLOB_API_URL_PREFIX, |
|
||||
VITE_GLOB_UPLOAD_URL, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Development model |
|
||||
*/ |
|
||||
export const devMode = 'development'; |
|
||||
|
|
||||
/** |
|
||||
* @description: Production mode |
|
||||
*/ |
|
||||
export const prodMode = 'production'; |
|
||||
|
|
||||
/** |
|
||||
* @description: Get environment variables |
|
||||
* @returns: |
|
||||
* @example: |
|
||||
*/ |
|
||||
export function getEnv(): string { |
|
||||
return import.meta.env.MODE; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Is it a development mode |
|
||||
* @returns: |
|
||||
* @example: |
|
||||
*/ |
|
||||
export function isDevMode(): boolean { |
|
||||
return import.meta.env.DEV; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Is it a production mode |
|
||||
* @returns: |
|
||||
* @example: |
|
||||
*/ |
|
||||
export function isProdMode(): boolean { |
|
||||
return import.meta.env.PROD; |
|
||||
} |
|
||||
@ -1,42 +0,0 @@ |
|||||
import ResizeObserver from 'resize-observer-polyfill'; |
|
||||
|
|
||||
const isServer = typeof window === 'undefined'; |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
function resizeHandler(entries: any[]) { |
|
||||
for (const entry of entries) { |
|
||||
const listeners = entry.target.__resizeListeners__ || []; |
|
||||
if (listeners.length) { |
|
||||
listeners.forEach((fn: () => any) => { |
|
||||
fn(); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
export function addResizeListener(element: any, fn: () => any) { |
|
||||
if (isServer) return; |
|
||||
if (!element.__resizeListeners__) { |
|
||||
element.__resizeListeners__ = []; |
|
||||
element.__ro__ = new ResizeObserver(resizeHandler); |
|
||||
element.__ro__.observe(element); |
|
||||
} |
|
||||
element.__resizeListeners__.push(fn); |
|
||||
} |
|
||||
|
|
||||
/* istanbul ignore next */ |
|
||||
export function removeResizeListener(element: any, fn: () => any) { |
|
||||
if (!element || !element.__resizeListeners__) return; |
|
||||
element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); |
|
||||
if (!element.__resizeListeners__.length) { |
|
||||
element.__ro__.disconnect(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function triggerWindowResize() { |
|
||||
const event = document.createEvent('HTMLEvents'); |
|
||||
event.initEvent('resize', true, true); |
|
||||
(event as any).eventType = 'message'; |
|
||||
window.dispatchEvent(event); |
|
||||
} |
|
||||
@ -1,63 +0,0 @@ |
|||||
import { |
|
||||
defineAsyncComponent, |
|
||||
// FunctionalComponent, CSSProperties
|
|
||||
} from 'vue'; |
|
||||
import { Spin } from 'ant-design-vue'; |
|
||||
import { noop } from '/@/utils/index'; |
|
||||
|
|
||||
// const Loading: FunctionalComponent<{ size: 'small' | 'default' | 'large' }> = (props) => {
|
|
||||
// const style: CSSProperties = {
|
|
||||
// position: 'absolute',
|
|
||||
// display: 'flex',
|
|
||||
// justifyContent: 'center',
|
|
||||
// alignItems: 'center',
|
|
||||
// };
|
|
||||
// return (
|
|
||||
// <div style={style}>
|
|
||||
// <Spin spinning={true} size={props.size} />
|
|
||||
// </div>
|
|
||||
// );
|
|
||||
// };
|
|
||||
|
|
||||
interface Options { |
|
||||
size?: 'default' | 'small' | 'large'; |
|
||||
delay?: number; |
|
||||
timeout?: number; |
|
||||
loading?: boolean; |
|
||||
retry?: boolean; |
|
||||
} |
|
||||
|
|
||||
export function createAsyncComponent(loader: Fn, options: Options = {}) { |
|
||||
const { size = 'small', delay = 100, timeout = 30000, loading = false, retry = true } = options; |
|
||||
return defineAsyncComponent({ |
|
||||
loader, |
|
||||
loadingComponent: loading ? <Spin spinning={true} size={size} /> : undefined, |
|
||||
// The error component will be displayed if a timeout is
|
|
||||
// provided and exceeded. Default: Infinity.
|
|
||||
// TODO
|
|
||||
timeout, |
|
||||
// errorComponent
|
|
||||
// Defining if component is suspensible. Default: true.
|
|
||||
// suspensible: false,
|
|
||||
delay, |
|
||||
/** |
|
||||
* |
|
||||
* @param {*} error Error message object |
|
||||
* @param {*} retry A function that indicating whether the async component should retry when the loader promise rejects |
|
||||
* @param {*} fail End of failure |
|
||||
* @param {*} attempts Maximum allowed retries number |
|
||||
*/ |
|
||||
onError: !retry |
|
||||
? noop |
|
||||
: (error, retry, fail, attempts) => { |
|
||||
if (error.message.match(/fetch/) && attempts <= 3) { |
|
||||
// retry on fetch errors, 3 max attempts
|
|
||||
retry(); |
|
||||
} else { |
|
||||
// Note that retry/fail are like resolve/reject of a promise:
|
|
||||
// one of them must be called for the error handling to continue.
|
|
||||
fail(); |
|
||||
} |
|
||||
}, |
|
||||
}); |
|
||||
} |
|
||||
@ -1,41 +0,0 @@ |
|||||
/** |
|
||||
* @description: base64 to blob |
|
||||
*/ |
|
||||
export function dataURLtoBlob(base64Buf: string): Blob { |
|
||||
const arr = base64Buf.split(','); |
|
||||
const typeItem = arr[0]; |
|
||||
const mime = typeItem.match(/:(.*?);/)![1]; |
|
||||
const bstr = atob(arr[1]); |
|
||||
let n = bstr.length; |
|
||||
const u8arr = new Uint8Array(n); |
|
||||
while (n--) { |
|
||||
u8arr[n] = bstr.charCodeAt(n); |
|
||||
} |
|
||||
return new Blob([u8arr], { type: mime }); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* img url to base64 |
|
||||
* @param url |
|
||||
*/ |
|
||||
export function urlToBase64(url: string, mineType?: string): Promise<string> { |
|
||||
return new Promise((resolve, reject) => { |
|
||||
let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement>; |
|
||||
const ctx = canvas!.getContext('2d'); |
|
||||
|
|
||||
const img = new Image(); |
|
||||
img.crossOrigin = ''; |
|
||||
img.onload = function () { |
|
||||
if (!canvas || !ctx) { |
|
||||
return reject(); |
|
||||
} |
|
||||
canvas.height = img.height; |
|
||||
canvas.width = img.width; |
|
||||
ctx.drawImage(img, 0, 0); |
|
||||
const dataURL = canvas.toDataURL(mineType || 'image/png'); |
|
||||
canvas = null; |
|
||||
resolve(dataURL); |
|
||||
}; |
|
||||
img.src = url; |
|
||||
}); |
|
||||
} |
|
||||
@ -1,99 +0,0 @@ |
|||||
import { openWindow } from '..'; |
|
||||
import { dataURLtoBlob, urlToBase64 } from './base64Conver'; |
|
||||
|
|
||||
/** |
|
||||
* Download online pictures |
|
||||
* @param url |
|
||||
* @param filename |
|
||||
* @param mime |
|
||||
* @param bom |
|
||||
*/ |
|
||||
export function downloadByOnlineUrl(url: string, filename: string, mime?: string, bom?: BlobPart) { |
|
||||
urlToBase64(url).then((base64) => { |
|
||||
downloadByBase64(base64, filename, mime, bom); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Download pictures based on base64 |
|
||||
* @param buf |
|
||||
* @param filename |
|
||||
* @param mime |
|
||||
* @param bom |
|
||||
*/ |
|
||||
export function downloadByBase64(buf: string, filename: string, mime?: string, bom?: BlobPart) { |
|
||||
const base64Buf = dataURLtoBlob(buf); |
|
||||
downloadByData(base64Buf, filename, mime, bom); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Download according to the background interface file stream |
|
||||
* @param {*} data |
|
||||
* @param {*} filename |
|
||||
* @param {*} mime |
|
||||
* @param {*} bom |
|
||||
*/ |
|
||||
export function downloadByData(data: BlobPart, filename: string, mime?: string, bom?: BlobPart) { |
|
||||
const blobData = typeof bom !== 'undefined' ? [bom, data] : [data]; |
|
||||
const blob = new Blob(blobData, { type: mime || 'application/octet-stream' }); |
|
||||
if (typeof window.navigator.msSaveBlob !== 'undefined') { |
|
||||
window.navigator.msSaveBlob(blob, filename); |
|
||||
} else { |
|
||||
const blobURL = window.URL.createObjectURL(blob); |
|
||||
const tempLink = document.createElement('a'); |
|
||||
tempLink.style.display = 'none'; |
|
||||
tempLink.href = blobURL; |
|
||||
tempLink.setAttribute('download', filename); |
|
||||
if (typeof tempLink.download === 'undefined') { |
|
||||
tempLink.setAttribute('target', '_blank'); |
|
||||
} |
|
||||
document.body.appendChild(tempLink); |
|
||||
tempLink.click(); |
|
||||
document.body.removeChild(tempLink); |
|
||||
window.URL.revokeObjectURL(blobURL); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Download file according to file address |
|
||||
* @param {*} sUrl |
|
||||
*/ |
|
||||
export function downloadByUrl({ |
|
||||
url, |
|
||||
target = '_blank', |
|
||||
fileName, |
|
||||
}: { |
|
||||
url: string; |
|
||||
target?: TargetContext; |
|
||||
fileName?: string; |
|
||||
}): boolean { |
|
||||
const isChrome = window.navigator.userAgent.toLowerCase().indexOf('chrome') > -1; |
|
||||
const isSafari = window.navigator.userAgent.toLowerCase().indexOf('safari') > -1; |
|
||||
|
|
||||
if (/(iP)/g.test(window.navigator.userAgent)) { |
|
||||
console.error('Your browser does not support download!'); |
|
||||
return false; |
|
||||
} |
|
||||
if (isChrome || isSafari) { |
|
||||
const link = document.createElement('a'); |
|
||||
link.href = url; |
|
||||
link.target = target; |
|
||||
|
|
||||
if (link.download !== undefined) { |
|
||||
link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length); |
|
||||
} |
|
||||
|
|
||||
if (document.createEvent) { |
|
||||
const e = document.createEvent('MouseEvents'); |
|
||||
e.initEvent('click', true, true); |
|
||||
link.dispatchEvent(e); |
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
if (url.indexOf('?') === -1) { |
|
||||
url += '?download'; |
|
||||
} |
|
||||
|
|
||||
openWindow(url, { target }); |
|
||||
return true; |
|
||||
} |
|
||||
@ -1,186 +0,0 @@ |
|||||
interface TreeHelperConfig { |
|
||||
id: string; |
|
||||
children: string; |
|
||||
pid: string; |
|
||||
} |
|
||||
const DEFAULT_CONFIG: TreeHelperConfig = { |
|
||||
id: 'id', |
|
||||
children: 'children', |
|
||||
pid: 'pid', |
|
||||
}; |
|
||||
|
|
||||
const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config); |
|
||||
|
|
||||
// tree from list
|
|
||||
export function listToTree<T = any>(list: any[], config: Partial<TreeHelperConfig> = {}): T[] { |
|
||||
const conf = getConfig(config) as TreeHelperConfig; |
|
||||
const nodeMap = new Map(); |
|
||||
const result: T[] = []; |
|
||||
const { id, children, pid } = conf; |
|
||||
|
|
||||
for (const node of list) { |
|
||||
node[children] = node[children] || []; |
|
||||
nodeMap.set(node[id], node); |
|
||||
} |
|
||||
for (const node of list) { |
|
||||
const parent = nodeMap.get(node[pid]); |
|
||||
(parent ? parent.children : result).push(node); |
|
||||
} |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
export function treeToList<T = any>(tree: any, config: Partial<TreeHelperConfig> = {}): T { |
|
||||
config = getConfig(config); |
|
||||
const { children } = config; |
|
||||
const result: any = [...tree]; |
|
||||
for (let i = 0; i < result.length; i++) { |
|
||||
if (!result[i][children!]) continue; |
|
||||
result.splice(i + 1, 0, ...result[i][children!]); |
|
||||
} |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
export function findNode<T = any>( |
|
||||
tree: any, |
|
||||
func: Fn, |
|
||||
config: Partial<TreeHelperConfig> = {} |
|
||||
): T | null { |
|
||||
config = getConfig(config); |
|
||||
const { children } = config; |
|
||||
const list = [...tree]; |
|
||||
for (const node of list) { |
|
||||
if (func(node)) return node; |
|
||||
node[children!] && list.push(...node[children!]); |
|
||||
} |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
export function findNodeAll<T = any>( |
|
||||
tree: any, |
|
||||
func: Fn, |
|
||||
config: Partial<TreeHelperConfig> = {} |
|
||||
): T[] { |
|
||||
config = getConfig(config); |
|
||||
const { children } = config; |
|
||||
const list = [...tree]; |
|
||||
const result: T[] = []; |
|
||||
for (const node of list) { |
|
||||
func(node) && result.push(node); |
|
||||
node[children!] && list.push(...node[children!]); |
|
||||
} |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
export function findPath<T = any>( |
|
||||
tree: any, |
|
||||
func: Fn, |
|
||||
config: Partial<TreeHelperConfig> = {} |
|
||||
): T | T[] | null { |
|
||||
config = getConfig(config); |
|
||||
const path: T[] = []; |
|
||||
const list = [...tree]; |
|
||||
const visitedSet = new Set(); |
|
||||
const { children } = config; |
|
||||
while (list.length) { |
|
||||
const node = list[0]; |
|
||||
if (visitedSet.has(node)) { |
|
||||
path.pop(); |
|
||||
list.shift(); |
|
||||
} else { |
|
||||
visitedSet.add(node); |
|
||||
node[children!] && list.unshift(...node[children!]); |
|
||||
path.push(node); |
|
||||
if (func(node)) { |
|
||||
return path; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
export function findPathAll(tree: any, func: Fn, config: Partial<TreeHelperConfig> = {}) { |
|
||||
config = getConfig(config); |
|
||||
const path: any[] = []; |
|
||||
const list = [...tree]; |
|
||||
const result: any[] = []; |
|
||||
const visitedSet = new Set(), |
|
||||
{ children } = config; |
|
||||
while (list.length) { |
|
||||
const node = list[0]; |
|
||||
if (visitedSet.has(node)) { |
|
||||
path.pop(); |
|
||||
list.shift(); |
|
||||
} else { |
|
||||
visitedSet.add(node); |
|
||||
node[children!] && list.unshift(...node[children!]); |
|
||||
path.push(node); |
|
||||
func(node) && result.push([...path]); |
|
||||
} |
|
||||
} |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
export function filter<T = any>( |
|
||||
tree: T[], |
|
||||
func: (n: T) => boolean, |
|
||||
config: Partial<TreeHelperConfig> = {} |
|
||||
): T[] { |
|
||||
config = getConfig(config); |
|
||||
const children = config.children as string; |
|
||||
function listFilter(list: T[]) { |
|
||||
return list |
|
||||
.map((node: any) => ({ ...node })) |
|
||||
.filter((node) => { |
|
||||
node[children] = node[children] && listFilter(node[children]); |
|
||||
return func(node) || (node[children] && node[children].length); |
|
||||
}); |
|
||||
} |
|
||||
return listFilter(tree); |
|
||||
} |
|
||||
|
|
||||
export function forEach<T = any>( |
|
||||
tree: T[], |
|
||||
func: (n: T) => any, |
|
||||
config: Partial<TreeHelperConfig> = {} |
|
||||
): void { |
|
||||
config = getConfig(config); |
|
||||
const list: any[] = [...tree]; |
|
||||
const { children } = config; |
|
||||
for (let i = 0; i < list.length; i++) { |
|
||||
func(list[i]); |
|
||||
children && list[i][children] && list.splice(i + 1, 0, ...list[i][children]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Extract tree specified structure |
|
||||
*/ |
|
||||
export function treeMap<T = any>(treeData: T[], opt: { children?: string; conversion: Fn }): T[] { |
|
||||
return treeData.map((item) => treeMapEach(item, opt)); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Extract tree specified structure |
|
||||
*/ |
|
||||
export function treeMapEach( |
|
||||
data: any, |
|
||||
{ children = 'children', conversion }: { children?: string; conversion: Fn } |
|
||||
) { |
|
||||
const haveChildren = Array.isArray(data[children]) && data[children].length > 0; |
|
||||
const conversionData = conversion(data) || {}; |
|
||||
if (haveChildren) { |
|
||||
return { |
|
||||
...conversionData, |
|
||||
[children]: data[children].map((i: number) => |
|
||||
treeMapEach(i, { |
|
||||
children, |
|
||||
conversion, |
|
||||
}) |
|
||||
), |
|
||||
}; |
|
||||
} else { |
|
||||
return { |
|
||||
...conversionData, |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
@ -1,35 +0,0 @@ |
|||||
import { Slots } from 'vue'; |
|
||||
import { isFunction } from '/@/utils/is'; |
|
||||
|
|
||||
/** |
|
||||
* @description: Get slot to prevent empty error |
|
||||
*/ |
|
||||
export function getSlot(slots: Slots, slot = 'default', data?: any) { |
|
||||
if (!slots || !Reflect.has(slots, slot)) { |
|
||||
return null; |
|
||||
} |
|
||||
if (!isFunction(slots[slot])) { |
|
||||
console.error(`${slot} is not a function!`); |
|
||||
return null; |
|
||||
} |
|
||||
const slotFn = slots[slot]; |
|
||||
if (!slotFn) return null; |
|
||||
return slotFn(data); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* extends slots |
|
||||
* @param slots |
|
||||
* @param excludeKeys |
|
||||
*/ |
|
||||
export function extendSlots(slots: Slots, excludeKeys: string[] = []) { |
|
||||
const slotKeys = Object.keys(slots); |
|
||||
const ret: any = {}; |
|
||||
slotKeys.map((key) => { |
|
||||
if (excludeKeys.includes(key)) { |
|
||||
return null; |
|
||||
} |
|
||||
ret[key] = () => getSlot(slots, key); |
|
||||
}); |
|
||||
return ret; |
|
||||
} |
|
||||
@ -0,0 +1,17 @@ |
|||||
|
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; |
||||
|
|
||||
|
const service = axios.create({ |
||||
|
// baseURL: '/gateway/logistics'
|
||||
|
}); |
||||
|
|
||||
|
service.interceptors.request.use((config: AxiosRequestConfig) => { |
||||
|
|
||||
|
return config; |
||||
|
}); |
||||
|
|
||||
|
service.interceptors.response.use((response: AxiosResponse) => { |
||||
|
|
||||
|
return response; |
||||
|
}) |
||||
|
|
||||
|
export default service; |
||||
@ -1,226 +0,0 @@ |
|||||
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios'; |
|
||||
import type { RequestOptions, Result, UploadFileParams } from './types'; |
|
||||
import type { CreateAxiosOptions } from './axiosTransform'; |
|
||||
|
|
||||
import axios from 'axios'; |
|
||||
import qs from 'qs'; |
|
||||
import { AxiosCanceler } from './axiosCancel'; |
|
||||
import { isFunction } from '/@/utils/is'; |
|
||||
import { cloneDeep } from 'lodash-es'; |
|
||||
|
|
||||
import { errorResult } from './const'; |
|
||||
import { ContentTypeEnum } from '/@/enums/httpEnum'; |
|
||||
import { RequestEnum } from '../../../enums/httpEnum'; |
|
||||
|
|
||||
export * from './axiosTransform'; |
|
||||
|
|
||||
/** |
|
||||
* @description: axios module |
|
||||
*/ |
|
||||
export class VAxios { |
|
||||
private axiosInstance: AxiosInstance; |
|
||||
private readonly options: CreateAxiosOptions; |
|
||||
|
|
||||
constructor(options: CreateAxiosOptions) { |
|
||||
this.options = options; |
|
||||
this.axiosInstance = axios.create(options); |
|
||||
this.setupInterceptors(); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Create axios instance |
|
||||
*/ |
|
||||
private createAxios(config: CreateAxiosOptions): void { |
|
||||
this.axiosInstance = axios.create(config); |
|
||||
} |
|
||||
|
|
||||
private getTransform() { |
|
||||
const { transform } = this.options; |
|
||||
return transform; |
|
||||
} |
|
||||
|
|
||||
getAxios(): AxiosInstance { |
|
||||
return this.axiosInstance; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Reconfigure axios |
|
||||
*/ |
|
||||
configAxios(config: CreateAxiosOptions) { |
|
||||
if (!this.axiosInstance) { |
|
||||
return; |
|
||||
} |
|
||||
this.createAxios(config); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Set general header |
|
||||
*/ |
|
||||
setHeader(headers: any): void { |
|
||||
if (!this.axiosInstance) { |
|
||||
return; |
|
||||
} |
|
||||
Object.assign(this.axiosInstance.defaults.headers, headers); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Interceptor configuration |
|
||||
*/ |
|
||||
private setupInterceptors() { |
|
||||
const transform = this.getTransform(); |
|
||||
if (!transform) { |
|
||||
return; |
|
||||
} |
|
||||
const { |
|
||||
requestInterceptors, |
|
||||
requestInterceptorsCatch, |
|
||||
responseInterceptors, |
|
||||
responseInterceptorsCatch, |
|
||||
} = transform; |
|
||||
|
|
||||
const axiosCanceler = new AxiosCanceler(); |
|
||||
|
|
||||
// Request interceptor configuration processing
|
|
||||
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => { |
|
||||
// If cancel repeat request is turned on, then cancel repeat request is prohibited
|
|
||||
const { |
|
||||
headers: { ignoreCancelToken }, |
|
||||
} = config; |
|
||||
|
|
||||
const ignoreCancel = |
|
||||
ignoreCancelToken !== undefined |
|
||||
? ignoreCancelToken |
|
||||
: this.options.requestOptions?.ignoreCancelToken; |
|
||||
|
|
||||
!ignoreCancel && axiosCanceler.addPending(config); |
|
||||
if (requestInterceptors && isFunction(requestInterceptors)) { |
|
||||
config = requestInterceptors(config); |
|
||||
} |
|
||||
return config; |
|
||||
}, undefined); |
|
||||
|
|
||||
// Request interceptor error capture
|
|
||||
requestInterceptorsCatch && |
|
||||
isFunction(requestInterceptorsCatch) && |
|
||||
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch); |
|
||||
|
|
||||
// Response result interceptor processing
|
|
||||
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => { |
|
||||
res && axiosCanceler.removePending(res.config); |
|
||||
if (responseInterceptors && isFunction(responseInterceptors)) { |
|
||||
res = responseInterceptors(res); |
|
||||
} |
|
||||
return res; |
|
||||
}, undefined); |
|
||||
|
|
||||
// Response result interceptor error capture
|
|
||||
responseInterceptorsCatch && |
|
||||
isFunction(responseInterceptorsCatch) && |
|
||||
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: File Upload |
|
||||
*/ |
|
||||
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) { |
|
||||
const formData = new window.FormData(); |
|
||||
|
|
||||
if (params.data) { |
|
||||
Object.keys(params.data).forEach((key) => { |
|
||||
if (!params.data) return; |
|
||||
const value = params.data[key]; |
|
||||
if (Array.isArray(value)) { |
|
||||
value.forEach((item) => { |
|
||||
formData.append(`${key}[]`, item); |
|
||||
}); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
formData.append(key, params.data[key]); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
formData.append(params.name || 'file', params.file, params.filename); |
|
||||
|
|
||||
return this.axiosInstance.request<T>({ |
|
||||
...config, |
|
||||
method: 'POST', |
|
||||
data: formData, |
|
||||
headers: { |
|
||||
'Content-type': ContentTypeEnum.FORM_DATA, |
|
||||
ignoreCancelToken: true, |
|
||||
}, |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// support form-data
|
|
||||
supportFormData(config: AxiosRequestConfig) { |
|
||||
const headers = config.headers || this.options.headers; |
|
||||
const contentType = headers?.['Content-Type'] || headers?.['content-type']; |
|
||||
|
|
||||
if ( |
|
||||
contentType !== ContentTypeEnum.FORM_URLENCODED || |
|
||||
!Reflect.has(config, 'data') || |
|
||||
config.method?.toUpperCase() === RequestEnum.GET |
|
||||
) { |
|
||||
return config; |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
...config, |
|
||||
data: qs.stringify(config.data, { arrayFormat: 'brackets' }), |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
|
||||
return this.request({ ...config, method: 'GET' }, options); |
|
||||
} |
|
||||
|
|
||||
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
|
||||
return this.request({ ...config, method: 'POST' }, options); |
|
||||
} |
|
||||
|
|
||||
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
|
||||
return this.request({ ...config, method: 'PUT' }, options); |
|
||||
} |
|
||||
|
|
||||
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
|
||||
return this.request({ ...config, method: 'DELETE' }, options); |
|
||||
} |
|
||||
|
|
||||
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
|
||||
let conf: AxiosRequestConfig = cloneDeep(config); |
|
||||
const transform = this.getTransform(); |
|
||||
|
|
||||
const { requestOptions } = this.options; |
|
||||
|
|
||||
const opt: RequestOptions = Object.assign({}, requestOptions, options); |
|
||||
|
|
||||
const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {}; |
|
||||
if (beforeRequestHook && isFunction(beforeRequestHook)) { |
|
||||
conf = beforeRequestHook(conf, opt); |
|
||||
} |
|
||||
|
|
||||
conf = this.supportFormData(conf); |
|
||||
|
|
||||
return new Promise((resolve, reject) => { |
|
||||
this.axiosInstance |
|
||||
.request<any, AxiosResponse<Result>>(conf) |
|
||||
.then((res: AxiosResponse<Result>) => { |
|
||||
if (transformRequestHook && isFunction(transformRequestHook)) { |
|
||||
const ret = transformRequestHook(res, opt); |
|
||||
ret !== errorResult ? resolve(ret) : reject(new Error('request error!')); |
|
||||
return; |
|
||||
} |
|
||||
resolve((res as unknown) as Promise<T>); |
|
||||
}) |
|
||||
.catch((e: Error) => { |
|
||||
if (requestCatchHook && isFunction(requestCatchHook)) { |
|
||||
reject(requestCatchHook(e)); |
|
||||
return; |
|
||||
} |
|
||||
reject(e); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
@ -1,61 +0,0 @@ |
|||||
import type { AxiosRequestConfig, Canceler } from 'axios'; |
|
||||
import axios from 'axios'; |
|
||||
|
|
||||
import { isFunction } from '/@/utils/is'; |
|
||||
|
|
||||
// Used to store the identification and cancellation function of each request
|
|
||||
let pendingMap = new Map<string, Canceler>(); |
|
||||
|
|
||||
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&'); |
|
||||
|
|
||||
export class AxiosCanceler { |
|
||||
/** |
|
||||
* Add request |
|
||||
* @param {Object} config |
|
||||
*/ |
|
||||
addPending(config: AxiosRequestConfig) { |
|
||||
this.removePending(config); |
|
||||
const url = getPendingUrl(config); |
|
||||
config.cancelToken = |
|
||||
config.cancelToken || |
|
||||
new axios.CancelToken((cancel) => { |
|
||||
if (!pendingMap.has(url)) { |
|
||||
// If there is no current request in pending, add it
|
|
||||
pendingMap.set(url, cancel); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: Clear all pending |
|
||||
*/ |
|
||||
removeAllPending() { |
|
||||
pendingMap.forEach((cancel) => { |
|
||||
cancel && isFunction(cancel) && cancel(); |
|
||||
}); |
|
||||
pendingMap.clear(); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Removal request |
|
||||
* @param {Object} config |
|
||||
*/ |
|
||||
removePending(config: AxiosRequestConfig) { |
|
||||
const url = getPendingUrl(config); |
|
||||
|
|
||||
if (pendingMap.has(url)) { |
|
||||
// If there is a current request identifier in pending,
|
|
||||
// the current request needs to be cancelled and removed
|
|
||||
const cancel = pendingMap.get(url); |
|
||||
cancel && cancel(url); |
|
||||
pendingMap.delete(url); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* @description: reset |
|
||||
*/ |
|
||||
reset(): void { |
|
||||
pendingMap = new Map<string, Canceler>(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,49 +0,0 @@ |
|||||
/** |
|
||||
* Data processing class, can be configured according to the project |
|
||||
*/ |
|
||||
import type { AxiosRequestConfig, AxiosResponse } from 'axios'; |
|
||||
import type { RequestOptions, Result } from './types'; |
|
||||
|
|
||||
export interface CreateAxiosOptions extends AxiosRequestConfig { |
|
||||
prefixUrl?: string; |
|
||||
transform?: AxiosTransform; |
|
||||
requestOptions?: RequestOptions; |
|
||||
} |
|
||||
|
|
||||
export abstract class AxiosTransform { |
|
||||
/** |
|
||||
* @description: Process configuration before request |
|
||||
* @description: Process configuration before request |
|
||||
*/ |
|
||||
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig; |
|
||||
|
|
||||
/** |
|
||||
* @description: Request successfully processed |
|
||||
*/ |
|
||||
transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any; |
|
||||
|
|
||||
/** |
|
||||
* @description: 请求失败处理 |
|
||||
*/ |
|
||||
requestCatchHook?: (e: Error) => Promise<any>; |
|
||||
|
|
||||
/** |
|
||||
* @description: 请求之前的拦截器 |
|
||||
*/ |
|
||||
requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig; |
|
||||
|
|
||||
/** |
|
||||
* @description: 请求之后的拦截器 |
|
||||
*/ |
|
||||
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>; |
|
||||
|
|
||||
/** |
|
||||
* @description: 请求之前的拦截器错误处理 |
|
||||
*/ |
|
||||
requestInterceptorsCatch?: (error: Error) => void; |
|
||||
|
|
||||
/** |
|
||||
* @description: 请求之后的拦截器错误处理 |
|
||||
*/ |
|
||||
responseInterceptorsCatch?: (error: Error) => void; |
|
||||
} |
|
||||
@ -1,55 +0,0 @@ |
|||||
import { useMessage } from '/@/hooks/web/useMessage'; |
|
||||
import { useI18n } from '/@/hooks/web/useI18n'; |
|
||||
import router from '/@/router'; |
|
||||
import { PageEnum } from '/@/enums/pageEnum'; |
|
||||
|
|
||||
const { createMessage } = useMessage(); |
|
||||
|
|
||||
const error = createMessage.error!; |
|
||||
export function checkStatus(status: number, msg: string): void { |
|
||||
const { t } = useI18n(); |
|
||||
switch (status) { |
|
||||
case 400: |
|
||||
error(`${msg}`); |
|
||||
break; |
|
||||
// 401: Not logged in
|
|
||||
// Jump to the login page if not logged in, and carry the path of the current page
|
|
||||
// Return to the current page after successful login. This step needs to be operated on the login page.
|
|
||||
case 401: |
|
||||
error(t('sys.api.errMsg401')); |
|
||||
router.push(PageEnum.BASE_LOGIN); |
|
||||
break; |
|
||||
case 403: |
|
||||
error(t('sys.api.errMsg403')); |
|
||||
break; |
|
||||
// 404请求不存在
|
|
||||
case 404: |
|
||||
error(t('sys.api.errMsg404')); |
|
||||
break; |
|
||||
case 405: |
|
||||
error(t('sys.api.errMsg405')); |
|
||||
break; |
|
||||
case 408: |
|
||||
error(t('sys.api.errMsg408')); |
|
||||
break; |
|
||||
case 500: |
|
||||
error(t('sys.api.errMsg500')); |
|
||||
break; |
|
||||
case 501: |
|
||||
error(t('sys.api.errMsg501')); |
|
||||
break; |
|
||||
case 502: |
|
||||
error(t('sys.api.errMsg502')); |
|
||||
break; |
|
||||
case 503: |
|
||||
error(t('sys.api.errMsg503')); |
|
||||
break; |
|
||||
case 504: |
|
||||
error(t('sys.api.errMsg504')); |
|
||||
break; |
|
||||
case 505: |
|
||||
error(t('sys.api.errMsg505')); |
|
||||
break; |
|
||||
default: |
|
||||
} |
|
||||
} |
|
||||
@ -1,2 +0,0 @@ |
|||||
// The interface return value data cannot be this, otherwise the request will be judged as a failure
|
|
||||
export const errorResult = '__ERROR_RESULT__'; |
|
||||
@ -1,42 +0,0 @@ |
|||||
import { isObject, isString } from '/@/utils/is'; |
|
||||
|
|
||||
export function createNow<T extends boolean>( |
|
||||
join: boolean, |
|
||||
restful: T |
|
||||
): T extends true ? string : object; |
|
||||
|
|
||||
export function createNow(join: boolean, restful = false): string | object { |
|
||||
if (!join) { |
|
||||
return restful ? '' : {}; |
|
||||
} |
|
||||
const now = new Date().getTime(); |
|
||||
if (restful) { |
|
||||
return `?_t=${now}`; |
|
||||
} |
|
||||
return { _t: now }; |
|
||||
} |
|
||||
|
|
||||
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm'; |
|
||||
/** |
|
||||
* @description: Format request parameter time |
|
||||
*/ |
|
||||
export function formatRequestDate(params: any) { |
|
||||
for (const key in params) { |
|
||||
if (params[key] && params[key]._isAMomentObject) { |
|
||||
params[key] = params[key].format(DATE_TIME_FORMAT); |
|
||||
} |
|
||||
if (isString(key)) { |
|
||||
const value = params[key]; |
|
||||
if (value) { |
|
||||
try { |
|
||||
params[key] = isString(value) ? value.trim() : value; |
|
||||
} catch (error) { |
|
||||
throw new Error(error); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
if (isObject(params[key])) { |
|
||||
formatRequestDate(params[key]); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,222 +0,0 @@ |
|||||
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
|
|
||||
// The axios configuration can be changed according to the project, just change the file, other files can be left unchanged
|
|
||||
|
|
||||
import type { AxiosResponse } from 'axios'; |
|
||||
import type { RequestOptions, Result } from './types'; |
|
||||
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform'; |
|
||||
|
|
||||
import { VAxios } from './Axios'; |
|
||||
import { checkStatus } from './checkStatus'; |
|
||||
|
|
||||
import { useGlobSetting } from '/@/hooks/setting'; |
|
||||
import { useMessage } from '/@/hooks/web/useMessage'; |
|
||||
|
|
||||
import { RequestEnum, ResultEnum, ContentTypeEnum } from '/@/enums/httpEnum'; |
|
||||
|
|
||||
import { isString } from '/@/utils/is'; |
|
||||
import { getToken } from '/@/utils/auth'; |
|
||||
import { setObjToUrlParams, deepMerge } from '/@/utils'; |
|
||||
import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog'; |
|
||||
|
|
||||
import { errorResult } from './const'; |
|
||||
import { useI18n } from '/@/hooks/web/useI18n'; |
|
||||
import { createNow, formatRequestDate } from './helper'; |
|
||||
|
|
||||
const globSetting = useGlobSetting(); |
|
||||
const prefix = globSetting.urlPrefix; |
|
||||
const { createMessage, createErrorModal } = useMessage(); |
|
||||
|
|
||||
/** |
|
||||
* @description: 数据处理,方便区分多种处理方式 |
|
||||
*/ |
|
||||
const transform: AxiosTransform = { |
|
||||
/** |
|
||||
* @description: 处理请求数据 |
|
||||
*/ |
|
||||
transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => { |
|
||||
const { t } = useI18n(); |
|
||||
const { isTransformRequestResult } = options; |
|
||||
// 不进行任何处理,直接返回
|
|
||||
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
|
||||
if (!isTransformRequestResult) { |
|
||||
return res.data; |
|
||||
} |
|
||||
// 错误的时候返回
|
|
||||
|
|
||||
const { data } = res; |
|
||||
if (!data) { |
|
||||
// return '[HTTP] Request has no return value';
|
|
||||
return errorResult; |
|
||||
} |
|
||||
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
|
|
||||
const { code, result, message } = data; |
|
||||
|
|
||||
// 这里逻辑可以根据项目进行修改
|
|
||||
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS; |
|
||||
if (!hasSuccess) { |
|
||||
if (message) { |
|
||||
// errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
|
||||
if (options.errorMessageMode === 'modal') { |
|
||||
createErrorModal({ title: t('sys.api.errorTip'), content: message }); |
|
||||
} else if (options.errorMessageMode === 'message') { |
|
||||
createMessage.error(message); |
|
||||
} |
|
||||
} |
|
||||
Promise.reject(new Error(message)); |
|
||||
return errorResult; |
|
||||
} |
|
||||
|
|
||||
// 接口请求成功,直接返回结果
|
|
||||
if (code === ResultEnum.SUCCESS) { |
|
||||
return result; |
|
||||
} |
|
||||
// 接口请求错误,统一提示错误信息
|
|
||||
if (code === ResultEnum.ERROR) { |
|
||||
if (message) { |
|
||||
createMessage.error(data.message); |
|
||||
Promise.reject(new Error(message)); |
|
||||
} else { |
|
||||
const msg = t('sys.api.errorMessage'); |
|
||||
createMessage.error(msg); |
|
||||
Promise.reject(new Error(msg)); |
|
||||
} |
|
||||
return errorResult; |
|
||||
} |
|
||||
// 登录超时
|
|
||||
if (code === ResultEnum.TIMEOUT) { |
|
||||
const timeoutMsg = t('sys.api.timeoutMessage'); |
|
||||
createErrorModal({ |
|
||||
title: t('sys.api.operationFailed'), |
|
||||
content: timeoutMsg, |
|
||||
}); |
|
||||
Promise.reject(new Error(timeoutMsg)); |
|
||||
return errorResult; |
|
||||
} |
|
||||
return errorResult; |
|
||||
}, |
|
||||
|
|
||||
// 请求之前处理config
|
|
||||
beforeRequestHook: (config, options) => { |
|
||||
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true } = options; |
|
||||
|
|
||||
if (joinPrefix) { |
|
||||
config.url = `${prefix}${config.url}`; |
|
||||
} |
|
||||
|
|
||||
if (apiUrl && isString(apiUrl)) { |
|
||||
config.url = `${apiUrl}${config.url}`; |
|
||||
} |
|
||||
const params = config.params || {}; |
|
||||
if (config.method?.toUpperCase() === RequestEnum.GET) { |
|
||||
if (!isString(params)) { |
|
||||
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
|
|
||||
config.params = Object.assign(params || {}, createNow(joinTime, false)); |
|
||||
} else { |
|
||||
// 兼容restful风格
|
|
||||
config.url = config.url + params + `${createNow(joinTime, true)}`; |
|
||||
config.params = undefined; |
|
||||
} |
|
||||
} else { |
|
||||
if (!isString(params)) { |
|
||||
formatDate && formatRequestDate(params); |
|
||||
config.data = params; |
|
||||
config.params = undefined; |
|
||||
if (joinParamsToUrl) { |
|
||||
config.url = setObjToUrlParams(config.url as string, config.data); |
|
||||
} |
|
||||
} else { |
|
||||
// 兼容restful风格
|
|
||||
config.url = config.url + params; |
|
||||
config.params = undefined; |
|
||||
} |
|
||||
} |
|
||||
return config; |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* @description: 请求拦截器处理 |
|
||||
*/ |
|
||||
requestInterceptors: (config) => { |
|
||||
// 请求之前处理config
|
|
||||
const token = getToken(); |
|
||||
if (token) { |
|
||||
// jwt token
|
|
||||
config.headers.Authorization = token; |
|
||||
} |
|
||||
return config; |
|
||||
}, |
|
||||
|
|
||||
/** |
|
||||
* @description: 响应错误处理 |
|
||||
*/ |
|
||||
responseInterceptorsCatch: (error: any) => { |
|
||||
const { t } = useI18n(); |
|
||||
const errorLogStore = useErrorLogStoreWithOut(); |
|
||||
errorLogStore.addAjaxErrorInfo(error); |
|
||||
const { response, code, message } = error || {}; |
|
||||
const msg: string = response?.data?.error?.message ?? ''; |
|
||||
const err: string = error?.toString?.() ?? ''; |
|
||||
try { |
|
||||
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) { |
|
||||
createMessage.error(t('sys.api.apiTimeoutMessage')); |
|
||||
} |
|
||||
if (err?.includes('Network Error')) { |
|
||||
createErrorModal({ |
|
||||
title: t('sys.api.networkException'), |
|
||||
content: t('sys.api.networkExceptionMsg'), |
|
||||
}); |
|
||||
} |
|
||||
} catch (error) { |
|
||||
throw new Error(error); |
|
||||
} |
|
||||
checkStatus(error?.response?.status, msg); |
|
||||
return Promise.reject(error); |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
function createAxios(opt?: Partial<CreateAxiosOptions>) { |
|
||||
return new VAxios( |
|
||||
deepMerge( |
|
||||
{ |
|
||||
timeout: 10 * 1000, |
|
||||
// 基础接口地址
|
|
||||
// baseURL: globSetting.apiUrl,
|
|
||||
// 接口可能会有通用的地址部分,可以统一抽取出来
|
|
||||
prefixUrl: prefix, |
|
||||
headers: { 'Content-Type': ContentTypeEnum.JSON }, |
|
||||
// 如果是form-data格式
|
|
||||
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
|
|
||||
// 数据处理方式
|
|
||||
transform, |
|
||||
// 配置项,下面的选项都可以在独立的接口请求中覆盖
|
|
||||
requestOptions: { |
|
||||
// 默认将prefix 添加到url
|
|
||||
joinPrefix: true, |
|
||||
// 需要对返回数据进行处理
|
|
||||
isTransformRequestResult: true, |
|
||||
// post请求的时候添加参数到url
|
|
||||
joinParamsToUrl: false, |
|
||||
// 格式化提交参数时间
|
|
||||
formatDate: true, |
|
||||
// 消息提示类型
|
|
||||
errorMessageMode: 'message', |
|
||||
// 接口地址
|
|
||||
apiUrl: globSetting.apiUrl, |
|
||||
// 是否加入时间戳
|
|
||||
joinTime: true, |
|
||||
// 忽略重复请求
|
|
||||
ignoreCancelToken: true, |
|
||||
}, |
|
||||
}, |
|
||||
opt || {} |
|
||||
) |
|
||||
); |
|
||||
} |
|
||||
export const defHttp = createAxios(); |
|
||||
|
|
||||
// other api url
|
|
||||
// export const otherHttp = createAxios({
|
|
||||
// requestOptions: {
|
|
||||
// apiUrl: 'xxx',
|
|
||||
// },
|
|
||||
// });
|
|
||||
@ -1,39 +0,0 @@ |
|||||
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined; |
|
||||
|
|
||||
export interface RequestOptions { |
|
||||
// Splicing request parameters to url
|
|
||||
joinParamsToUrl?: boolean; |
|
||||
// Format request parameter time
|
|
||||
formatDate?: boolean; |
|
||||
// Whether to process the request result
|
|
||||
isTransformRequestResult?: boolean; |
|
||||
// Whether to join url
|
|
||||
joinPrefix?: boolean; |
|
||||
// Interface address, use the default apiUrl if you leave it blank
|
|
||||
apiUrl?: string; |
|
||||
// Error message prompt type
|
|
||||
errorMessageMode?: ErrorMessageMode; |
|
||||
// Whether to add a timestamp
|
|
||||
joinTime?: boolean; |
|
||||
ignoreCancelToken?: boolean; |
|
||||
} |
|
||||
|
|
||||
export interface Result<T = any> { |
|
||||
code: number; |
|
||||
type: 'success' | 'error' | 'warning'; |
|
||||
message: string; |
|
||||
result: T; |
|
||||
} |
|
||||
|
|
||||
// multipart/form-data: upload file
|
|
||||
export interface UploadFileParams { |
|
||||
// Other parameters
|
|
||||
data?: Recordable; |
|
||||
// File parameter interface field name
|
|
||||
name?: string; |
|
||||
// file name
|
|
||||
file: File | Blob; |
|
||||
// file name
|
|
||||
filename?: string; |
|
||||
[key: string]: any; |
|
||||
} |
|
||||
@ -1,78 +0,0 @@ |
|||||
import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router'; |
|
||||
import { unref } from 'vue'; |
|
||||
import { isObject } from '/@/utils/is'; |
|
||||
|
|
||||
export const noop = () => {}; |
|
||||
|
|
||||
/** |
|
||||
* @description: Set ui mount node |
|
||||
*/ |
|
||||
export function getPopupContainer(node?: HTMLElement): HTMLElement { |
|
||||
return (node?.parentNode as HTMLElement) ?? document.body; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Add the object as a parameter to the URL |
|
||||
* @param baseUrl url |
|
||||
* @param obj |
|
||||
* @returns {string} |
|
||||
* eg: |
|
||||
* let obj = {a: '3', b: '4'} |
|
||||
* setObjToUrlParams('www.baidu.com', obj) |
|
||||
* ==>www.baidu.com?a=3&b=4 |
|
||||
*/ |
|
||||
export function setObjToUrlParams(baseUrl: string, obj: any): string { |
|
||||
let parameters = ''; |
|
||||
for (const key in obj) { |
|
||||
parameters += key + '=' + encodeURIComponent(obj[key]) + '&'; |
|
||||
} |
|
||||
parameters = parameters.replace(/&$/, ''); |
|
||||
return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters; |
|
||||
} |
|
||||
|
|
||||
export function deepMerge<T = any>(src: any = {}, target: any = {}): T { |
|
||||
let key: string; |
|
||||
for (key in target) { |
|
||||
src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]); |
|
||||
} |
|
||||
return src; |
|
||||
} |
|
||||
|
|
||||
export function openWindow( |
|
||||
url: string, |
|
||||
opt?: { target?: TargetContext | string; noopener?: boolean; noreferrer?: boolean } |
|
||||
) { |
|
||||
const { target = '__blank', noopener = true, noreferrer = true } = opt || {}; |
|
||||
const feature: string[] = []; |
|
||||
|
|
||||
noopener && feature.push('noopener=yes'); |
|
||||
noreferrer && feature.push('noreferrer=yes'); |
|
||||
|
|
||||
window.open(url, target, feature.join(',')); |
|
||||
} |
|
||||
|
|
||||
// dynamic use hook props
|
|
||||
export function getDynamicProps<T, U>(props: T): Partial<U> { |
|
||||
const ret: Recordable = {}; |
|
||||
|
|
||||
Object.keys(props).map((key) => { |
|
||||
ret[key] = unref((props as Recordable)[key]); |
|
||||
}); |
|
||||
|
|
||||
return ret as Partial<U>; |
|
||||
} |
|
||||
|
|
||||
export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized { |
|
||||
if (!route) return route; |
|
||||
const { matched, ...opt } = route; |
|
||||
return { |
|
||||
...opt, |
|
||||
matched: (matched |
|
||||
? matched.map((item) => ({ |
|
||||
meta: item.meta, |
|
||||
name: item.name, |
|
||||
path: item.path, |
|
||||
})) |
|
||||
: undefined) as RouteRecordNormalized[], |
|
||||
}; |
|
||||
} |
|
||||
@ -1,94 +0,0 @@ |
|||||
const toString = Object.prototype.toString; |
|
||||
|
|
||||
export function is(val: unknown, type: string) { |
|
||||
return toString.call(val) === `[object ${type}]`; |
|
||||
} |
|
||||
|
|
||||
export function isDef<T = unknown>(val?: T): val is T { |
|
||||
return typeof val !== 'undefined'; |
|
||||
} |
|
||||
|
|
||||
export function isUnDef<T = unknown>(val?: T): val is T { |
|
||||
return !isDef(val); |
|
||||
} |
|
||||
|
|
||||
export function isObject(val: any): val is Record<any, any> { |
|
||||
return val !== null && is(val, 'Object'); |
|
||||
} |
|
||||
|
|
||||
export function isEmpty<T = unknown>(val: T): val is T { |
|
||||
if (isArray(val) || isString(val)) { |
|
||||
return val.length === 0; |
|
||||
} |
|
||||
|
|
||||
if (val instanceof Map || val instanceof Set) { |
|
||||
return val.size === 0; |
|
||||
} |
|
||||
|
|
||||
if (isObject(val)) { |
|
||||
return Object.keys(val).length === 0; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
export function isDate(val: unknown): val is Date { |
|
||||
return is(val, 'Date'); |
|
||||
} |
|
||||
|
|
||||
export function isNull(val: unknown): val is null { |
|
||||
return val === null; |
|
||||
} |
|
||||
|
|
||||
export function isNullAndUnDef(val: unknown): val is null | undefined { |
|
||||
return isUnDef(val) && isNull(val); |
|
||||
} |
|
||||
|
|
||||
export function isNullOrUnDef(val: unknown): val is null | undefined { |
|
||||
return isUnDef(val) || isNull(val); |
|
||||
} |
|
||||
|
|
||||
export function isNumber(val: unknown): val is number { |
|
||||
return is(val, 'Number'); |
|
||||
} |
|
||||
|
|
||||
export function isPromise<T = any>(val: unknown): val is Promise<T> { |
|
||||
return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch); |
|
||||
} |
|
||||
|
|
||||
export function isString(val: unknown): val is string { |
|
||||
return is(val, 'String'); |
|
||||
} |
|
||||
|
|
||||
export function isFunction(val: unknown): val is Function { |
|
||||
return typeof val === 'function'; |
|
||||
} |
|
||||
|
|
||||
export function isBoolean(val: unknown): val is boolean { |
|
||||
return is(val, 'Boolean'); |
|
||||
} |
|
||||
|
|
||||
export function isRegExp(val: unknown): val is RegExp { |
|
||||
return is(val, 'RegExp'); |
|
||||
} |
|
||||
|
|
||||
export function isArray(val: any): val is Array<any> { |
|
||||
return val && Array.isArray(val); |
|
||||
} |
|
||||
|
|
||||
export function isWindow(val: any): val is Window { |
|
||||
return typeof window !== 'undefined' && is(val, 'Window'); |
|
||||
} |
|
||||
|
|
||||
export function isElement(val: unknown): val is Element { |
|
||||
return isObject(val) && !!val.tagName; |
|
||||
} |
|
||||
|
|
||||
export const isServer = typeof window === 'undefined'; |
|
||||
|
|
||||
export const isClient = !isServer; |
|
||||
|
|
||||
export function isUrl(path: string): boolean { |
|
||||
const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; |
|
||||
return reg.test(path); |
|
||||
} |
|
||||
@ -1,53 +0,0 @@ |
|||||
import * as echarts from 'echarts/core'; |
|
||||
|
|
||||
import { |
|
||||
BarChart, |
|
||||
LineChart, |
|
||||
PieChart, |
|
||||
MapChart, |
|
||||
PictorialBarChart, |
|
||||
RadarChart, |
|
||||
} from 'echarts/charts'; |
|
||||
|
|
||||
import { |
|
||||
TitleComponent, |
|
||||
TooltipComponent, |
|
||||
GridComponent, |
|
||||
PolarComponent, |
|
||||
AriaComponent, |
|
||||
ParallelComponent, |
|
||||
LegendComponent, |
|
||||
RadarComponent, |
|
||||
ToolboxComponent, |
|
||||
DataZoomComponent, |
|
||||
VisualMapComponent, |
|
||||
TimelineComponent, |
|
||||
CalendarComponent, |
|
||||
} from 'echarts/components'; |
|
||||
|
|
||||
import { SVGRenderer } from 'echarts/renderers'; |
|
||||
|
|
||||
echarts.use([ |
|
||||
LegendComponent, |
|
||||
TitleComponent, |
|
||||
TooltipComponent, |
|
||||
GridComponent, |
|
||||
PolarComponent, |
|
||||
AriaComponent, |
|
||||
ParallelComponent, |
|
||||
BarChart, |
|
||||
LineChart, |
|
||||
PieChart, |
|
||||
MapChart, |
|
||||
RadarChart, |
|
||||
SVGRenderer, |
|
||||
PictorialBarChart, |
|
||||
RadarComponent, |
|
||||
ToolboxComponent, |
|
||||
DataZoomComponent, |
|
||||
VisualMapComponent, |
|
||||
TimelineComponent, |
|
||||
CalendarComponent, |
|
||||
]); |
|
||||
|
|
||||
export default echarts; |
|
||||
@ -1,9 +0,0 @@ |
|||||
const projectName = import.meta.env.VITE_GLOB_APP_TITLE; |
|
||||
|
|
||||
export function warn(message: string) { |
|
||||
console.warn(`[${projectName} warn]:${message}`); |
|
||||
} |
|
||||
|
|
||||
export function error(message: string) { |
|
||||
throw new Error(`[${projectName} error]:${message}`); |
|
||||
} |
|
||||
@ -1,73 +0,0 @@ |
|||||
/** |
|
||||
* Mitt: Tiny functional event emitter / pubsub |
|
||||
* |
|
||||
* @name mitt |
|
||||
* @param {Array} [all] Optional array of event names to registered handler functions |
|
||||
* @returns {Function} The function's instance |
|
||||
*/ |
|
||||
export default class Mitt { |
|
||||
private cache: Map<string | Symbol, Array<(...data: any) => void>>; |
|
||||
constructor(all = []) { |
|
||||
// A Map of event names to registered handler functions.
|
|
||||
this.cache = new Map(all); |
|
||||
} |
|
||||
|
|
||||
once(type: string | Symbol, handler: Fn) { |
|
||||
const decor = (...args: any[]) => { |
|
||||
handler && handler.apply(this, args); |
|
||||
this.off(type, decor); |
|
||||
}; |
|
||||
this.on(type, decor); |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Register an event handler for the given type. |
|
||||
* |
|
||||
* @param {string|symbol} type Type of event to listen for, or `"*"` for all events |
|
||||
* @param {Function} handler Function to call in response to given event |
|
||||
*/ |
|
||||
on(type: string | Symbol, handler: Fn) { |
|
||||
const handlers = this.cache?.get(type); |
|
||||
const added = handlers && handlers.push(handler); |
|
||||
if (!added) { |
|
||||
this.cache.set(type, [handler]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Remove an event handler for the given type. |
|
||||
* |
|
||||
* @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` |
|
||||
* @param {Function} handler Handler function to remove |
|
||||
*/ |
|
||||
off(type: string | Symbol, handler: Fn) { |
|
||||
const handlers = this.cache.get(type); |
|
||||
if (handlers) { |
|
||||
handlers.splice(handlers.indexOf(handler) >>> 0, 1); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Invoke all handlers for the given type. |
|
||||
* If present, `"*"` handlers are invoked after type-matched handlers. |
|
||||
* |
|
||||
* Note: Manually firing "*" handlers is not supported. |
|
||||
* |
|
||||
* @param {string|symbol} type The event type to invoke |
|
||||
* @param {*} [evt] Any value (object is recommended and powerful), passed to each handler |
|
||||
*/ |
|
||||
emit(type: string | Symbol, evt?: any) { |
|
||||
for (const handler of (this.cache.get(type) || []).slice()) handler(evt); |
|
||||
for (const handler of (this.cache.get('*') || []).slice()) handler(type, evt); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Remove all event handlers. |
|
||||
* |
|
||||
* Note: This will also remove event handlers passed via `mitt(all: EventHandlerMap)`. |
|
||||
*/ |
|
||||
clear() { |
|
||||
this.cache.clear(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,40 +0,0 @@ |
|||||
import { CSSProperties, VNodeChild } from 'vue'; |
|
||||
import { createTypes, VueTypeValidableDef, VueTypesInterface } from 'vue-types'; |
|
||||
|
|
||||
export type VueNode = VNodeChild | JSX.Element; |
|
||||
|
|
||||
type PropTypes = VueTypesInterface & { |
|
||||
readonly style: VueTypeValidableDef<CSSProperties>; |
|
||||
readonly VNodeChild: VueTypeValidableDef<VueNode>; |
|
||||
// readonly trueBool: VueTypeValidableDef<boolean>;
|
|
||||
}; |
|
||||
|
|
||||
const propTypes = createTypes({ |
|
||||
func: undefined, |
|
||||
bool: undefined, |
|
||||
string: undefined, |
|
||||
number: undefined, |
|
||||
object: undefined, |
|
||||
integer: undefined, |
|
||||
}) as PropTypes; |
|
||||
|
|
||||
propTypes.extend([ |
|
||||
{ |
|
||||
name: 'style', |
|
||||
getter: true, |
|
||||
type: [String, Object], |
|
||||
default: undefined, |
|
||||
}, |
|
||||
{ |
|
||||
name: 'VNodeChild', |
|
||||
getter: true, |
|
||||
type: undefined, |
|
||||
}, |
|
||||
// {
|
|
||||
// name: 'trueBool',
|
|
||||
// getter: true,
|
|
||||
// type: Boolean,
|
|
||||
// default: true,
|
|
||||
// },
|
|
||||
]); |
|
||||
export { propTypes }; |
|
||||
@ -1,28 +0,0 @@ |
|||||
const hexList: string[] = []; |
|
||||
for (let i = 0; i <= 15; i++) { |
|
||||
hexList[i] = i.toString(16); |
|
||||
} |
|
||||
|
|
||||
export function buildUUID(): string { |
|
||||
let uuid = ''; |
|
||||
for (let i = 1; i <= 36; i++) { |
|
||||
if (i === 9 || i === 14 || i === 19 || i === 24) { |
|
||||
uuid += '-'; |
|
||||
} else if (i === 15) { |
|
||||
uuid += 4; |
|
||||
} else if (i === 20) { |
|
||||
uuid += hexList[(Math.random() * 4) | 8]; |
|
||||
} else { |
|
||||
uuid += hexList[(Math.random() * 16) | 0]; |
|
||||
} |
|
||||
} |
|
||||
return uuid.replace(/-/g, ''); |
|
||||
} |
|
||||
|
|
||||
let unique = 0; |
|
||||
export function buildShortUUID(prefix = ''): string { |
|
||||
const time = Date.now(); |
|
||||
const random = Math.floor(Math.random() * 1000000000); |
|
||||
unique++; |
|
||||
return prefix + '_' + random + unique + String(time); |
|
||||
} |
|
||||
@ -0,0 +1,141 @@ |
|||||
|
<template> |
||||
|
<div class="flex flex-col"> |
||||
|
<!-- 标题 --> |
||||
|
<Toolbar /> |
||||
|
|
||||
|
<div class="flex flex-col flex-1 con"> |
||||
|
<div class="flex justify-between floor1"> |
||||
|
<!-- 温度 --> |
||||
|
<Temperature /> |
||||
|
<!-- 湿度 --> |
||||
|
<Humidity /> |
||||
|
</div> |
||||
|
|
||||
|
<div class="flex-1 iframe-wrap"> |
||||
|
<!-- <iframe src="https://www.tall.wiki/wl/0"></iframe> --> |
||||
|
</div> |
||||
|
|
||||
|
<div class="flex justify-between floor3"> |
||||
|
<!-- 吞土量实时统计 --> |
||||
|
<Amount /> |
||||
|
<!-- 仓库热力图 --> |
||||
|
<Heat /> |
||||
|
</div> |
||||
|
<!-- 曲线对比图 --> |
||||
|
<div class="floor4"> |
||||
|
<Curve /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<!-- 悬浮监控 --> |
||||
|
<img @click="show = true" class="monitor" src="../assets/monitor.png" style /> |
||||
|
<img @click="show = false" class="vedio" src="../assets/vedio.png" v-show="show" /> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { defineComponent } from 'vue' |
||||
|
import Toolbar from 'comp/toolbar.vue' |
||||
|
import Temperature from 'comp/temperature.vue' |
||||
|
import Humidity from 'comp/humidity.vue' |
||||
|
import Amount from 'comp/amount.vue' |
||||
|
import Heat from 'comp/heat.vue' |
||||
|
import Curve from 'comp/curve.vue' |
||||
|
import { getWarehouses } from 'api/api' |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
components: { |
||||
|
Toolbar, |
||||
|
Temperature, |
||||
|
Humidity, |
||||
|
Amount, |
||||
|
Heat, |
||||
|
Curve, |
||||
|
}, |
||||
|
data() { |
||||
|
return { show: false } |
||||
|
}, |
||||
|
created() { |
||||
|
this.getData() |
||||
|
}, |
||||
|
methods: { |
||||
|
async getData() { |
||||
|
const param = { parkId: 0 } |
||||
|
try { |
||||
|
const res = await getWarehouses(param) |
||||
|
console.log('res: ', res) |
||||
|
} catch (error) { |
||||
|
console.log('error: ', error) |
||||
|
} |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<style scoped> |
||||
|
.con { |
||||
|
margin-left: 20rem; |
||||
|
margin-right: 20rem; |
||||
|
} |
||||
|
|
||||
|
.floor1 { |
||||
|
width: 100%; |
||||
|
height: 390rem; |
||||
|
margin-top: 22rem; |
||||
|
} |
||||
|
|
||||
|
.floor3 { |
||||
|
width: 100%; |
||||
|
height: 390rem; |
||||
|
margin-bottom: 22rem; |
||||
|
} |
||||
|
|
||||
|
.floor4 { |
||||
|
width: 100%; |
||||
|
height: 390rem; |
||||
|
margin-bottom: 22rem; |
||||
|
} |
||||
|
|
||||
|
.monitor { |
||||
|
position: absolute; |
||||
|
right: 0; |
||||
|
top: 10%; |
||||
|
z-index: 99; |
||||
|
width: 48rem; |
||||
|
height: 50rem; |
||||
|
} |
||||
|
|
||||
|
.vedio { |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
left: 16%; |
||||
|
z-index: 99; |
||||
|
width: 720rem; |
||||
|
height: 495rem; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<style> |
||||
|
.box-bg { |
||||
|
width: 497rem; |
||||
|
height: 392rem; |
||||
|
background: url('../assets/box1.png') center no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
|
||||
|
.box2-bg { |
||||
|
width: 100%; |
||||
|
height: 392rem; |
||||
|
background: url('../assets/box2.png') center no-repeat; |
||||
|
background-size: 100% 100%; |
||||
|
} |
||||
|
.iframe-wrap { |
||||
|
padding-top: 20rem; |
||||
|
padding-bottom: 20rem; |
||||
|
} |
||||
|
iframe { |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
border: none !important; |
||||
|
} |
||||
|
</style> |
||||
@ -1,299 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Actions', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/actions') |
|
||||
}) |
|
||||
|
|
||||
// https://on.cypress.io/interacting-with-elements
|
|
||||
|
|
||||
it('.type() - type into a DOM element', () => { |
|
||||
// https://on.cypress.io/type
|
|
||||
cy.get('.action-email') |
|
||||
.type('fake@email.com').should('have.value', 'fake@email.com') |
|
||||
|
|
||||
// .type() with special character sequences
|
|
||||
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}') |
|
||||
.type('{del}{selectall}{backspace}') |
|
||||
|
|
||||
// .type() with key modifiers
|
|
||||
.type('{alt}{option}') //these are equivalent
|
|
||||
.type('{ctrl}{control}') //these are equivalent
|
|
||||
.type('{meta}{command}{cmd}') //these are equivalent
|
|
||||
.type('{shift}') |
|
||||
|
|
||||
// Delay each keypress by 0.1 sec
|
|
||||
.type('slow.typing@email.com', { delay: 100 }) |
|
||||
.should('have.value', 'slow.typing@email.com') |
|
||||
|
|
||||
cy.get('.action-disabled') |
|
||||
// Ignore error checking prior to type
|
|
||||
// like whether the input is visible or disabled
|
|
||||
.type('disabled error checking', { force: true }) |
|
||||
.should('have.value', 'disabled error checking') |
|
||||
}) |
|
||||
|
|
||||
it('.focus() - focus on a DOM element', () => { |
|
||||
// https://on.cypress.io/focus
|
|
||||
cy.get('.action-focus').focus() |
|
||||
.should('have.class', 'focus') |
|
||||
.prev().should('have.attr', 'style', 'color: orange;') |
|
||||
}) |
|
||||
|
|
||||
it('.blur() - blur off a DOM element', () => { |
|
||||
// https://on.cypress.io/blur
|
|
||||
cy.get('.action-blur').type('About to blur').blur() |
|
||||
.should('have.class', 'error') |
|
||||
.prev().should('have.attr', 'style', 'color: red;') |
|
||||
}) |
|
||||
|
|
||||
it('.clear() - clears an input or textarea element', () => { |
|
||||
// https://on.cypress.io/clear
|
|
||||
cy.get('.action-clear').type('Clear this text') |
|
||||
.should('have.value', 'Clear this text') |
|
||||
.clear() |
|
||||
.should('have.value', '') |
|
||||
}) |
|
||||
|
|
||||
it('.submit() - submit a form', () => { |
|
||||
// https://on.cypress.io/submit
|
|
||||
cy.get('.action-form') |
|
||||
.find('[type="text"]').type('HALFOFF') |
|
||||
|
|
||||
cy.get('.action-form').submit() |
|
||||
.next().should('contain', 'Your form has been submitted!') |
|
||||
}) |
|
||||
|
|
||||
it('.click() - click on a DOM element', () => { |
|
||||
// https://on.cypress.io/click
|
|
||||
cy.get('.action-btn').click() |
|
||||
|
|
||||
// You can click on 9 specific positions of an element:
|
|
||||
// -----------------------------------
|
|
||||
// | topLeft top topRight |
|
|
||||
// | |
|
|
||||
// | |
|
|
||||
// | |
|
|
||||
// | left center right |
|
|
||||
// | |
|
|
||||
// | |
|
|
||||
// | |
|
|
||||
// | bottomLeft bottom bottomRight |
|
|
||||
// -----------------------------------
|
|
||||
|
|
||||
// clicking in the center of the element is the default
|
|
||||
cy.get('#action-canvas').click() |
|
||||
|
|
||||
cy.get('#action-canvas').click('topLeft') |
|
||||
cy.get('#action-canvas').click('top') |
|
||||
cy.get('#action-canvas').click('topRight') |
|
||||
cy.get('#action-canvas').click('left') |
|
||||
cy.get('#action-canvas').click('right') |
|
||||
cy.get('#action-canvas').click('bottomLeft') |
|
||||
cy.get('#action-canvas').click('bottom') |
|
||||
cy.get('#action-canvas').click('bottomRight') |
|
||||
|
|
||||
// .click() accepts an x and y coordinate
|
|
||||
// that controls where the click occurs :)
|
|
||||
|
|
||||
cy.get('#action-canvas') |
|
||||
.click(80, 75) // click 80px on x coord and 75px on y coord
|
|
||||
.click(170, 75) |
|
||||
.click(80, 165) |
|
||||
.click(100, 185) |
|
||||
.click(125, 190) |
|
||||
.click(150, 185) |
|
||||
.click(170, 165) |
|
||||
|
|
||||
// click multiple elements by passing multiple: true
|
|
||||
cy.get('.action-labels>.label').click({ multiple: true }) |
|
||||
|
|
||||
// Ignore error checking prior to clicking
|
|
||||
cy.get('.action-opacity>.btn').click({ force: true }) |
|
||||
}) |
|
||||
|
|
||||
it('.dblclick() - double click on a DOM element', () => { |
|
||||
// https://on.cypress.io/dblclick
|
|
||||
|
|
||||
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
|
||||
// that hides the div and shows an input on double click
|
|
||||
cy.get('.action-div').dblclick().should('not.be.visible') |
|
||||
cy.get('.action-input-hidden').should('be.visible') |
|
||||
}) |
|
||||
|
|
||||
it('.rightclick() - right click on a DOM element', () => { |
|
||||
// https://on.cypress.io/rightclick
|
|
||||
|
|
||||
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
|
||||
// that hides the div and shows an input on right click
|
|
||||
cy.get('.rightclick-action-div').rightclick().should('not.be.visible') |
|
||||
cy.get('.rightclick-action-input-hidden').should('be.visible') |
|
||||
}) |
|
||||
|
|
||||
it('.check() - check a checkbox or radio element', () => { |
|
||||
// https://on.cypress.io/check
|
|
||||
|
|
||||
// By default, .check() will check all
|
|
||||
// matching checkbox or radio elements in succession, one after another
|
|
||||
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]') |
|
||||
.check().should('be.checked') |
|
||||
|
|
||||
cy.get('.action-radios [type="radio"]').not('[disabled]') |
|
||||
.check().should('be.checked') |
|
||||
|
|
||||
// .check() accepts a value argument
|
|
||||
cy.get('.action-radios [type="radio"]') |
|
||||
.check('radio1').should('be.checked') |
|
||||
|
|
||||
// .check() accepts an array of values
|
|
||||
cy.get('.action-multiple-checkboxes [type="checkbox"]') |
|
||||
.check(['checkbox1', 'checkbox2']).should('be.checked') |
|
||||
|
|
||||
// Ignore error checking prior to checking
|
|
||||
cy.get('.action-checkboxes [disabled]') |
|
||||
.check({ force: true }).should('be.checked') |
|
||||
|
|
||||
cy.get('.action-radios [type="radio"]') |
|
||||
.check('radio3', { force: true }).should('be.checked') |
|
||||
}) |
|
||||
|
|
||||
it('.uncheck() - uncheck a checkbox element', () => { |
|
||||
// https://on.cypress.io/uncheck
|
|
||||
|
|
||||
// By default, .uncheck() will uncheck all matching
|
|
||||
// checkbox elements in succession, one after another
|
|
||||
cy.get('.action-check [type="checkbox"]') |
|
||||
.not('[disabled]') |
|
||||
.uncheck().should('not.be.checked') |
|
||||
|
|
||||
// .uncheck() accepts a value argument
|
|
||||
cy.get('.action-check [type="checkbox"]') |
|
||||
.check('checkbox1') |
|
||||
.uncheck('checkbox1').should('not.be.checked') |
|
||||
|
|
||||
// .uncheck() accepts an array of values
|
|
||||
cy.get('.action-check [type="checkbox"]') |
|
||||
.check(['checkbox1', 'checkbox3']) |
|
||||
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked') |
|
||||
|
|
||||
// Ignore error checking prior to unchecking
|
|
||||
cy.get('.action-check [disabled]') |
|
||||
.uncheck({ force: true }).should('not.be.checked') |
|
||||
}) |
|
||||
|
|
||||
it('.select() - select an option in a <select> element', () => { |
|
||||
// https://on.cypress.io/select
|
|
||||
|
|
||||
// at first, no option should be selected
|
|
||||
cy.get('.action-select') |
|
||||
.should('have.value', '--Select a fruit--') |
|
||||
|
|
||||
// Select option(s) with matching text content
|
|
||||
cy.get('.action-select').select('apples') |
|
||||
// confirm the apples were selected
|
|
||||
// note that each value starts with "fr-" in our HTML
|
|
||||
cy.get('.action-select').should('have.value', 'fr-apples') |
|
||||
|
|
||||
cy.get('.action-select-multiple') |
|
||||
.select(['apples', 'oranges', 'bananas']) |
|
||||
// when getting multiple values, invoke "val" method first
|
|
||||
.invoke('val') |
|
||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) |
|
||||
|
|
||||
// Select option(s) with matching value
|
|
||||
cy.get('.action-select').select('fr-bananas') |
|
||||
// can attach an assertion right away to the element
|
|
||||
.should('have.value', 'fr-bananas') |
|
||||
|
|
||||
cy.get('.action-select-multiple') |
|
||||
.select(['fr-apples', 'fr-oranges', 'fr-bananas']) |
|
||||
.invoke('val') |
|
||||
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas']) |
|
||||
|
|
||||
// assert the selected values include oranges
|
|
||||
cy.get('.action-select-multiple') |
|
||||
.invoke('val').should('include', 'fr-oranges') |
|
||||
}) |
|
||||
|
|
||||
it('.scrollIntoView() - scroll an element into view', () => { |
|
||||
// https://on.cypress.io/scrollintoview
|
|
||||
|
|
||||
// normally all of these buttons are hidden,
|
|
||||
// because they're not within
|
|
||||
// the viewable area of their parent
|
|
||||
// (we need to scroll to see them)
|
|
||||
cy.get('#scroll-horizontal button') |
|
||||
.should('not.be.visible') |
|
||||
|
|
||||
// scroll the button into view, as if the user had scrolled
|
|
||||
cy.get('#scroll-horizontal button').scrollIntoView() |
|
||||
.should('be.visible') |
|
||||
|
|
||||
cy.get('#scroll-vertical button') |
|
||||
.should('not.be.visible') |
|
||||
|
|
||||
// Cypress handles the scroll direction needed
|
|
||||
cy.get('#scroll-vertical button').scrollIntoView() |
|
||||
.should('be.visible') |
|
||||
|
|
||||
cy.get('#scroll-both button') |
|
||||
.should('not.be.visible') |
|
||||
|
|
||||
// Cypress knows to scroll to the right and down
|
|
||||
cy.get('#scroll-both button').scrollIntoView() |
|
||||
.should('be.visible') |
|
||||
}) |
|
||||
|
|
||||
it('.trigger() - trigger an event on a DOM element', () => { |
|
||||
// https://on.cypress.io/trigger
|
|
||||
|
|
||||
// To interact with a range input (slider)
|
|
||||
// we need to set its value & trigger the
|
|
||||
// event to signal it changed
|
|
||||
|
|
||||
// Here, we invoke jQuery's val() method to set
|
|
||||
// the value and trigger the 'change' event
|
|
||||
cy.get('.trigger-input-range') |
|
||||
.invoke('val', 25) |
|
||||
.trigger('change') |
|
||||
.get('input[type=range]').siblings('p') |
|
||||
.should('have.text', '25') |
|
||||
}) |
|
||||
|
|
||||
it('cy.scrollTo() - scroll the window or element to a position', () => { |
|
||||
// https://on.cypress.io/scrollTo
|
|
||||
|
|
||||
// You can scroll to 9 specific positions of an element:
|
|
||||
// -----------------------------------
|
|
||||
// | topLeft top topRight |
|
|
||||
// | |
|
|
||||
// | |
|
|
||||
// | |
|
|
||||
// | left center right |
|
|
||||
// | |
|
|
||||
// | |
|
|
||||
// | |
|
|
||||
// | bottomLeft bottom bottomRight |
|
|
||||
// -----------------------------------
|
|
||||
|
|
||||
// if you chain .scrollTo() off of cy, we will
|
|
||||
// scroll the entire window
|
|
||||
cy.scrollTo('bottom') |
|
||||
|
|
||||
cy.get('#scrollable-horizontal').scrollTo('right') |
|
||||
|
|
||||
// or you can scroll to a specific coordinate:
|
|
||||
// (x axis, y axis) in pixels
|
|
||||
cy.get('#scrollable-vertical').scrollTo(250, 250) |
|
||||
|
|
||||
// or you can scroll to a specific percentage
|
|
||||
// of the (width, height) of the element
|
|
||||
cy.get('#scrollable-both').scrollTo('75%', '25%') |
|
||||
|
|
||||
// control the easing of the scroll (default is 'swing')
|
|
||||
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' }) |
|
||||
|
|
||||
// control the duration of the scroll (in ms)
|
|
||||
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 }) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,40 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Aliasing', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/aliasing') |
|
||||
}) |
|
||||
|
|
||||
it('.as() - alias a DOM element for later use', () => { |
|
||||
// https://on.cypress.io/as
|
|
||||
|
|
||||
// Alias a DOM element for use later
|
|
||||
// We don't have to traverse to the element
|
|
||||
// later in our code, we reference it with @
|
|
||||
|
|
||||
cy.get('.as-table').find('tbody>tr') |
|
||||
.first().find('td').first() |
|
||||
.find('button').as('firstBtn') |
|
||||
|
|
||||
// when we reference the alias, we place an
|
|
||||
// @ in front of its name
|
|
||||
cy.get('@firstBtn').click() |
|
||||
|
|
||||
cy.get('@firstBtn') |
|
||||
.should('have.class', 'btn-success') |
|
||||
.and('contain', 'Changed') |
|
||||
}) |
|
||||
|
|
||||
it('.as() - alias a route for later use', () => { |
|
||||
// Alias the route to wait for its response
|
|
||||
cy.server() |
|
||||
cy.route('GET', 'comments/*').as('getComment') |
|
||||
|
|
||||
// we have code that gets a comment when
|
|
||||
// the button is clicked in scripts.js
|
|
||||
cy.get('.network-btn').click() |
|
||||
|
|
||||
// https://on.cypress.io/wait
|
|
||||
cy.wait('@getComment').its('status').should('eq', 200) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,177 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Assertions', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/assertions') |
|
||||
}) |
|
||||
|
|
||||
describe('Implicit Assertions', () => { |
|
||||
it('.should() - make an assertion about the current subject', () => { |
|
||||
// https://on.cypress.io/should
|
|
||||
cy.get('.assertion-table') |
|
||||
.find('tbody tr:last') |
|
||||
.should('have.class', 'success') |
|
||||
.find('td') |
|
||||
.first() |
|
||||
// checking the text of the <td> element in various ways
|
|
||||
.should('have.text', 'Column content') |
|
||||
.should('contain', 'Column content') |
|
||||
.should('have.html', 'Column content') |
|
||||
// chai-jquery uses "is()" to check if element matches selector
|
|
||||
.should('match', 'td') |
|
||||
// to match text content against a regular expression
|
|
||||
// first need to invoke jQuery method text()
|
|
||||
// and then match using regular expression
|
|
||||
.invoke('text') |
|
||||
.should('match', /column content/i) |
|
||||
|
|
||||
// a better way to check element's text content against a regular expression
|
|
||||
// is to use "cy.contains"
|
|
||||
// https://on.cypress.io/contains
|
|
||||
cy.get('.assertion-table') |
|
||||
.find('tbody tr:last') |
|
||||
// finds first <td> element with text content matching regular expression
|
|
||||
.contains('td', /column content/i) |
|
||||
.should('be.visible') |
|
||||
|
|
||||
// for more information about asserting element's text
|
|
||||
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
|
||||
}) |
|
||||
|
|
||||
it('.and() - chain multiple assertions together', () => { |
|
||||
// https://on.cypress.io/and
|
|
||||
cy.get('.assertions-link') |
|
||||
.should('have.class', 'active') |
|
||||
.and('have.attr', 'href') |
|
||||
.and('include', 'cypress.io') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('Explicit Assertions', () => { |
|
||||
// https://on.cypress.io/assertions
|
|
||||
it('expect - make an assertion about a specified subject', () => { |
|
||||
// We can use Chai's BDD style assertions
|
|
||||
expect(true).to.be.true |
|
||||
const o = { foo: 'bar' } |
|
||||
|
|
||||
expect(o).to.equal(o) |
|
||||
expect(o).to.deep.equal({ foo: 'bar' }) |
|
||||
// matching text using regular expression
|
|
||||
expect('FooBar').to.match(/bar$/i) |
|
||||
}) |
|
||||
|
|
||||
it('pass your own callback function to should()', () => { |
|
||||
// Pass a function to should that can have any number
|
|
||||
// of explicit assertions within it.
|
|
||||
// The ".should(cb)" function will be retried
|
|
||||
// automatically until it passes all your explicit assertions or times out.
|
|
||||
cy.get('.assertions-p') |
|
||||
.find('p') |
|
||||
.should(($p) => { |
|
||||
// https://on.cypress.io/$
|
|
||||
// return an array of texts from all of the p's
|
|
||||
// @ts-ignore TS6133 unused variable
|
|
||||
const texts = $p.map((i, el) => Cypress.$(el).text()) |
|
||||
|
|
||||
// jquery map returns jquery object
|
|
||||
// and .get() convert this to simple array
|
|
||||
const paragraphs = texts.get() |
|
||||
|
|
||||
// array should have length of 3
|
|
||||
expect(paragraphs, 'has 3 paragraphs').to.have.length(3) |
|
||||
|
|
||||
// use second argument to expect(...) to provide clear
|
|
||||
// message with each assertion
|
|
||||
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([ |
|
||||
'Some text from first p', |
|
||||
'More text from second p', |
|
||||
'And even more text from third p', |
|
||||
]) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('finds element by class name regex', () => { |
|
||||
cy.get('.docs-header') |
|
||||
.find('div') |
|
||||
// .should(cb) callback function will be retried
|
|
||||
.should(($div) => { |
|
||||
expect($div).to.have.length(1) |
|
||||
|
|
||||
const className = $div[0].className |
|
||||
|
|
||||
expect(className).to.match(/heading-/) |
|
||||
}) |
|
||||
// .then(cb) callback is not retried,
|
|
||||
// it either passes or fails
|
|
||||
.then(($div) => { |
|
||||
expect($div, 'text content').to.have.text('Introduction') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('can throw any error', () => { |
|
||||
cy.get('.docs-header') |
|
||||
.find('div') |
|
||||
.should(($div) => { |
|
||||
if ($div.length !== 1) { |
|
||||
// you can throw your own errors
|
|
||||
throw new Error('Did not find 1 element') |
|
||||
} |
|
||||
|
|
||||
const className = $div[0].className |
|
||||
|
|
||||
if (!className.match(/heading-/)) { |
|
||||
throw new Error(`Could not find class "heading-" in ${className}`) |
|
||||
} |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('matches unknown text between two elements', () => { |
|
||||
/** |
|
||||
* Text from the first element. |
|
||||
* @type {string} |
|
||||
*/ |
|
||||
let text |
|
||||
|
|
||||
/** |
|
||||
* Normalizes passed text, |
|
||||
* useful before comparing text with spaces and different capitalization. |
|
||||
* @param {string} s Text to normalize |
|
||||
*/ |
|
||||
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase() |
|
||||
|
|
||||
cy.get('.two-elements') |
|
||||
.find('.first') |
|
||||
.then(($first) => { |
|
||||
// save text from the first element
|
|
||||
text = normalizeText($first.text()) |
|
||||
}) |
|
||||
|
|
||||
cy.get('.two-elements') |
|
||||
.find('.second') |
|
||||
.should(($div) => { |
|
||||
// we can massage text before comparing
|
|
||||
const secondText = normalizeText($div.text()) |
|
||||
|
|
||||
expect(secondText, 'second text').to.equal(text) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('assert - assert shape of an object', () => { |
|
||||
const person = { |
|
||||
name: 'Joe', |
|
||||
age: 20, |
|
||||
} |
|
||||
|
|
||||
assert.isObject(person, 'value is object') |
|
||||
}) |
|
||||
|
|
||||
it('retries the should callback until assertions pass', () => { |
|
||||
cy.get('#random-number') |
|
||||
.should(($div) => { |
|
||||
const n = parseFloat($div.text()) |
|
||||
|
|
||||
expect(n).to.be.gte(1).and.be.lte(10) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,97 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Connectors', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/connectors') |
|
||||
}) |
|
||||
|
|
||||
it('.each() - iterate over an array of elements', () => { |
|
||||
// https://on.cypress.io/each
|
|
||||
cy.get('.connectors-each-ul>li') |
|
||||
.each(($el, index, $list) => { |
|
||||
console.log($el, index, $list) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('.its() - get properties on the current subject', () => { |
|
||||
// https://on.cypress.io/its
|
|
||||
cy.get('.connectors-its-ul>li') |
|
||||
// calls the 'length' property yielding that value
|
|
||||
.its('length') |
|
||||
.should('be.gt', 2) |
|
||||
}) |
|
||||
|
|
||||
it('.invoke() - invoke a function on the current subject', () => { |
|
||||
// our div is hidden in our script.js
|
|
||||
// $('.connectors-div').hide()
|
|
||||
|
|
||||
// https://on.cypress.io/invoke
|
|
||||
cy.get('.connectors-div').should('be.hidden') |
|
||||
// call the jquery method 'show' on the 'div.container'
|
|
||||
.invoke('show') |
|
||||
.should('be.visible') |
|
||||
}) |
|
||||
|
|
||||
it('.spread() - spread an array as individual args to callback function', () => { |
|
||||
// https://on.cypress.io/spread
|
|
||||
const arr = ['foo', 'bar', 'baz'] |
|
||||
|
|
||||
cy.wrap(arr).spread((foo, bar, baz) => { |
|
||||
expect(foo).to.eq('foo') |
|
||||
expect(bar).to.eq('bar') |
|
||||
expect(baz).to.eq('baz') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
describe('.then()', () => { |
|
||||
it('invokes a callback function with the current subject', () => { |
|
||||
// https://on.cypress.io/then
|
|
||||
cy.get('.connectors-list > li') |
|
||||
.then(($lis) => { |
|
||||
expect($lis, '3 items').to.have.length(3) |
|
||||
expect($lis.eq(0), 'first item').to.contain('Walk the dog') |
|
||||
expect($lis.eq(1), 'second item').to.contain('Feed the cat') |
|
||||
expect($lis.eq(2), 'third item').to.contain('Write JavaScript') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('yields the returned value to the next command', () => { |
|
||||
cy.wrap(1) |
|
||||
.then((num) => { |
|
||||
expect(num).to.equal(1) |
|
||||
|
|
||||
return 2 |
|
||||
}) |
|
||||
.then((num) => { |
|
||||
expect(num).to.equal(2) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('yields the original subject without return', () => { |
|
||||
cy.wrap(1) |
|
||||
.then((num) => { |
|
||||
expect(num).to.equal(1) |
|
||||
// note that nothing is returned from this callback
|
|
||||
}) |
|
||||
.then((num) => { |
|
||||
// this callback receives the original unchanged value 1
|
|
||||
expect(num).to.equal(1) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('yields the value yielded by the last Cypress command inside', () => { |
|
||||
cy.wrap(1) |
|
||||
.then((num) => { |
|
||||
expect(num).to.equal(1) |
|
||||
// note how we run a Cypress command
|
|
||||
// the result yielded by this Cypress command
|
|
||||
// will be passed to the second ".then"
|
|
||||
cy.wrap(2) |
|
||||
}) |
|
||||
.then((num) => { |
|
||||
// this callback receives the value yielded by "cy.wrap(2)"
|
|
||||
expect(num).to.equal(2) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,77 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Cookies', () => { |
|
||||
beforeEach(() => { |
|
||||
Cypress.Cookies.debug(true) |
|
||||
|
|
||||
cy.visit('https://example.cypress.io/commands/cookies') |
|
||||
|
|
||||
// clear cookies again after visiting to remove
|
|
||||
// any 3rd party cookies picked up such as cloudflare
|
|
||||
cy.clearCookies() |
|
||||
}) |
|
||||
|
|
||||
it('cy.getCookie() - get a browser cookie', () => { |
|
||||
// https://on.cypress.io/getcookie
|
|
||||
cy.get('#getCookie .set-a-cookie').click() |
|
||||
|
|
||||
// cy.getCookie() yields a cookie object
|
|
||||
cy.getCookie('token').should('have.property', 'value', '123ABC') |
|
||||
}) |
|
||||
|
|
||||
it('cy.getCookies() - get browser cookies', () => { |
|
||||
// https://on.cypress.io/getcookies
|
|
||||
cy.getCookies().should('be.empty') |
|
||||
|
|
||||
cy.get('#getCookies .set-a-cookie').click() |
|
||||
|
|
||||
// cy.getCookies() yields an array of cookies
|
|
||||
cy.getCookies().should('have.length', 1).should((cookies) => { |
|
||||
// each cookie has these properties
|
|
||||
expect(cookies[0]).to.have.property('name', 'token') |
|
||||
expect(cookies[0]).to.have.property('value', '123ABC') |
|
||||
expect(cookies[0]).to.have.property('httpOnly', false) |
|
||||
expect(cookies[0]).to.have.property('secure', false) |
|
||||
expect(cookies[0]).to.have.property('domain') |
|
||||
expect(cookies[0]).to.have.property('path') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.setCookie() - set a browser cookie', () => { |
|
||||
// https://on.cypress.io/setcookie
|
|
||||
cy.getCookies().should('be.empty') |
|
||||
|
|
||||
cy.setCookie('foo', 'bar') |
|
||||
|
|
||||
// cy.getCookie() yields a cookie object
|
|
||||
cy.getCookie('foo').should('have.property', 'value', 'bar') |
|
||||
}) |
|
||||
|
|
||||
it('cy.clearCookie() - clear a browser cookie', () => { |
|
||||
// https://on.cypress.io/clearcookie
|
|
||||
cy.getCookie('token').should('be.null') |
|
||||
|
|
||||
cy.get('#clearCookie .set-a-cookie').click() |
|
||||
|
|
||||
cy.getCookie('token').should('have.property', 'value', '123ABC') |
|
||||
|
|
||||
// cy.clearCookies() yields null
|
|
||||
cy.clearCookie('token').should('be.null') |
|
||||
|
|
||||
cy.getCookie('token').should('be.null') |
|
||||
}) |
|
||||
|
|
||||
it('cy.clearCookies() - clear browser cookies', () => { |
|
||||
// https://on.cypress.io/clearcookies
|
|
||||
cy.getCookies().should('be.empty') |
|
||||
|
|
||||
cy.get('#clearCookies .set-a-cookie').click() |
|
||||
|
|
||||
cy.getCookies().should('have.length', 1) |
|
||||
|
|
||||
// cy.clearCookies() yields null
|
|
||||
cy.clearCookies() |
|
||||
|
|
||||
cy.getCookies().should('be.empty') |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,222 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Cypress.Commands', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
// https://on.cypress.io/custom-commands
|
|
||||
|
|
||||
it('.add() - create a custom command', () => { |
|
||||
Cypress.Commands.add('console', { |
|
||||
prevSubject: true, |
|
||||
}, (subject, method) => { |
|
||||
// the previous subject is automatically received
|
|
||||
// and the commands arguments are shifted
|
|
||||
|
|
||||
// allow us to change the console method used
|
|
||||
method = method || 'log' |
|
||||
|
|
||||
// log the subject to the console
|
|
||||
// @ts-ignore TS7017
|
|
||||
console[method]('The subject is', subject) |
|
||||
|
|
||||
// whatever we return becomes the new subject
|
|
||||
// we don't want to change the subject so
|
|
||||
// we return whatever was passed in
|
|
||||
return subject |
|
||||
}) |
|
||||
|
|
||||
// @ts-ignore TS2339
|
|
||||
cy.get('button').console('info').then(($button) => { |
|
||||
// subject is still $button
|
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.Cookies', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
// https://on.cypress.io/cookies
|
|
||||
it('.debug() - enable or disable debugging', () => { |
|
||||
Cypress.Cookies.debug(true) |
|
||||
|
|
||||
// Cypress will now log in the console when
|
|
||||
// cookies are set or cleared
|
|
||||
cy.setCookie('fakeCookie', '123ABC') |
|
||||
cy.clearCookie('fakeCookie') |
|
||||
cy.setCookie('fakeCookie', '123ABC') |
|
||||
cy.clearCookie('fakeCookie') |
|
||||
cy.setCookie('fakeCookie', '123ABC') |
|
||||
}) |
|
||||
|
|
||||
it('.preserveOnce() - preserve cookies by key', () => { |
|
||||
// normally cookies are reset after each test
|
|
||||
cy.getCookie('fakeCookie').should('not.be.ok') |
|
||||
|
|
||||
// preserving a cookie will not clear it when
|
|
||||
// the next test starts
|
|
||||
cy.setCookie('lastCookie', '789XYZ') |
|
||||
Cypress.Cookies.preserveOnce('lastCookie') |
|
||||
}) |
|
||||
|
|
||||
it('.defaults() - set defaults for all cookies', () => { |
|
||||
if (Number(Cypress.version.charAt(0)) < 5) return |
|
||||
|
|
||||
// now any cookie with the name 'session_id' will
|
|
||||
// not be cleared before each new test runs
|
|
||||
Cypress.Cookies.defaults({ |
|
||||
// @ts-ignore
|
|
||||
preserve: 'session_id', |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.Server', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
// Permanently override server options for
|
|
||||
// all instances of cy.server()
|
|
||||
|
|
||||
// https://on.cypress.io/cypress-server
|
|
||||
it('.defaults() - change default config of server', () => { |
|
||||
Cypress.Server.defaults({ |
|
||||
delay: 0, |
|
||||
force404: false, |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.arch', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
it('Get CPU architecture name of underlying OS', () => { |
|
||||
// https://on.cypress.io/arch
|
|
||||
expect(Cypress.arch).to.exist |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.config()', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
it('Get and set configuration options', () => { |
|
||||
// https://on.cypress.io/config
|
|
||||
let myConfig = Cypress.config() |
|
||||
|
|
||||
expect(myConfig).to.have.property('animationDistanceThreshold', 5) |
|
||||
expect(myConfig).to.have.property('baseUrl', null) |
|
||||
expect(myConfig).to.have.property('defaultCommandTimeout', 4000) |
|
||||
expect(myConfig).to.have.property('requestTimeout', 5000) |
|
||||
expect(myConfig).to.have.property('responseTimeout', 30000) |
|
||||
expect(myConfig).to.have.property('viewportHeight', 660) |
|
||||
expect(myConfig).to.have.property('viewportWidth', 1000) |
|
||||
expect(myConfig).to.have.property('pageLoadTimeout', 60000) |
|
||||
expect(myConfig).to.have.property('waitForAnimations', true) |
|
||||
|
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(60000) |
|
||||
|
|
||||
// this will change the config for the rest of your tests!
|
|
||||
Cypress.config('pageLoadTimeout', 20000) |
|
||||
|
|
||||
expect(Cypress.config('pageLoadTimeout')).to.eq(20000) |
|
||||
|
|
||||
Cypress.config('pageLoadTimeout', 60000) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.dom', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
// https://on.cypress.io/dom
|
|
||||
it('.isHidden() - determine if a DOM element is hidden', () => { |
|
||||
let hiddenP = Cypress.$('.dom-p p.hidden').get(0) |
|
||||
let visibleP = Cypress.$('.dom-p p.visible').get(0) |
|
||||
|
|
||||
// our first paragraph has css class 'hidden'
|
|
||||
expect(Cypress.dom.isHidden(hiddenP)).to.be.true |
|
||||
expect(Cypress.dom.isHidden(visibleP)).to.be.false |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.env()', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
// We can set environment variables for highly dynamic values
|
|
||||
|
|
||||
// https://on.cypress.io/environment-variables
|
|
||||
it('Get environment variables', () => { |
|
||||
// https://on.cypress.io/env
|
|
||||
// set multiple environment variables
|
|
||||
Cypress.env({ |
|
||||
host: 'veronica.dev.local', |
|
||||
api_server: 'http://localhost:8888/v1/', |
|
||||
}) |
|
||||
|
|
||||
// get environment variable
|
|
||||
expect(Cypress.env('host')).to.eq('veronica.dev.local') |
|
||||
|
|
||||
// set environment variable
|
|
||||
Cypress.env('api_server', 'http://localhost:8888/v2/') |
|
||||
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/') |
|
||||
|
|
||||
// get all environment variable
|
|
||||
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local') |
|
||||
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.log', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
it('Control what is printed to the Command Log', () => { |
|
||||
// https://on.cypress.io/cypress-log
|
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.platform', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
it('Get underlying OS name', () => { |
|
||||
// https://on.cypress.io/platform
|
|
||||
expect(Cypress.platform).to.be.exist |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.version', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
it('Get current version of Cypress being run', () => { |
|
||||
// https://on.cypress.io/version
|
|
||||
expect(Cypress.version).to.be.exist |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.spec', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/cypress-api') |
|
||||
}) |
|
||||
|
|
||||
it('Get current spec information', () => { |
|
||||
// https://on.cypress.io/spec
|
|
||||
// wrap the object so we can inspect it easily by clicking in the command log
|
|
||||
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute']) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,115 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
/// JSON fixture file can be loaded directly using
|
|
||||
// the built-in JavaScript bundler
|
|
||||
// @ts-ignore
|
|
||||
const requiredExample = require('../../fixtures/example') |
|
||||
|
|
||||
context('Files', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/files') |
|
||||
}) |
|
||||
|
|
||||
beforeEach(() => { |
|
||||
// load example.json fixture file and store
|
|
||||
// in the test context object
|
|
||||
cy.fixture('example.json').as('example') |
|
||||
}) |
|
||||
|
|
||||
it('cy.fixture() - load a fixture', () => { |
|
||||
// https://on.cypress.io/fixture
|
|
||||
|
|
||||
// Instead of writing a response inline you can
|
|
||||
// use a fixture file's content.
|
|
||||
|
|
||||
cy.server() |
|
||||
cy.fixture('example.json').as('comment') |
|
||||
// when application makes an Ajax request matching "GET comments/*"
|
|
||||
// Cypress will intercept it and reply with object
|
|
||||
// from the "comment" alias
|
|
||||
cy.route('GET', 'comments/*', '@comment').as('getComment') |
|
||||
|
|
||||
// we have code that gets a comment when
|
|
||||
// the button is clicked in scripts.js
|
|
||||
cy.get('.fixture-btn').click() |
|
||||
|
|
||||
cy.wait('@getComment').its('responseBody') |
|
||||
.should('have.property', 'name') |
|
||||
.and('include', 'Using fixtures to represent data') |
|
||||
|
|
||||
// you can also just write the fixture in the route
|
|
||||
cy.route('GET', 'comments/*', 'fixture:example.json').as('getComment') |
|
||||
|
|
||||
// we have code that gets a comment when
|
|
||||
// the button is clicked in scripts.js
|
|
||||
cy.get('.fixture-btn').click() |
|
||||
|
|
||||
cy.wait('@getComment').its('responseBody') |
|
||||
.should('have.property', 'name') |
|
||||
.and('include', 'Using fixtures to represent data') |
|
||||
|
|
||||
// or write fx to represent fixture
|
|
||||
// by default it assumes it's .json
|
|
||||
cy.route('GET', 'comments/*', 'fx:example').as('getComment') |
|
||||
|
|
||||
// we have code that gets a comment when
|
|
||||
// the button is clicked in scripts.js
|
|
||||
cy.get('.fixture-btn').click() |
|
||||
|
|
||||
cy.wait('@getComment').its('responseBody') |
|
||||
.should('have.property', 'name') |
|
||||
.and('include', 'Using fixtures to represent data') |
|
||||
}) |
|
||||
|
|
||||
it('cy.fixture() or require - load a fixture', function () { |
|
||||
// we are inside the "function () { ... }"
|
|
||||
// callback and can use test context object "this"
|
|
||||
// "this.example" was loaded in "beforeEach" function callback
|
|
||||
expect(this.example, 'fixture in the test context') |
|
||||
.to.deep.equal(requiredExample) |
|
||||
|
|
||||
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
|
||||
// @ts-ignore
|
|
||||
cy.wrap(this.example, 'fixture vs require') |
|
||||
.should('deep.equal', requiredExample) |
|
||||
}) |
|
||||
|
|
||||
it('cy.readFile() - read file contents', () => { |
|
||||
// https://on.cypress.io/readfile
|
|
||||
|
|
||||
// You can read a file and yield its contents
|
|
||||
// The filePath is relative to your project's root.
|
|
||||
cy.readFile('cypress.json').then((json) => { |
|
||||
expect(json).to.be.an('object') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.writeFile() - write to a file', () => { |
|
||||
// https://on.cypress.io/writefile
|
|
||||
|
|
||||
// You can write to a file
|
|
||||
|
|
||||
// Use a response from a request to automatically
|
|
||||
// generate a fixture file for use later
|
|
||||
cy.request('https://jsonplaceholder.cypress.io/users') |
|
||||
.then((response) => { |
|
||||
cy.writeFile('cypress/fixtures/users.json', response.body) |
|
||||
}) |
|
||||
|
|
||||
cy.fixture('users').should((users) => { |
|
||||
expect(users[0].name).to.exist |
|
||||
}) |
|
||||
|
|
||||
// JavaScript arrays and objects are stringified
|
|
||||
// and formatted into text.
|
|
||||
cy.writeFile('cypress/fixtures/profile.json', { |
|
||||
id: 8739, |
|
||||
name: 'Jane', |
|
||||
email: 'jane@example.com', |
|
||||
}) |
|
||||
|
|
||||
cy.fixture('profile').should((profile) => { |
|
||||
expect(profile.name).to.eq('Jane') |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,52 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Local Storage', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/local-storage') |
|
||||
}) |
|
||||
// Although local storage is automatically cleared
|
|
||||
// in between tests to maintain a clean state
|
|
||||
// sometimes we need to clear the local storage manually
|
|
||||
|
|
||||
it('cy.clearLocalStorage() - clear all data in local storage', () => { |
|
||||
// https://on.cypress.io/clearlocalstorage
|
|
||||
cy.get('.ls-btn').click().should(() => { |
|
||||
expect(localStorage.getItem('prop1')).to.eq('red') |
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue') |
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta') |
|
||||
}) |
|
||||
|
|
||||
// clearLocalStorage() yields the localStorage object
|
|
||||
cy.clearLocalStorage().should((ls) => { |
|
||||
expect(ls.getItem('prop1')).to.be.null |
|
||||
expect(ls.getItem('prop2')).to.be.null |
|
||||
expect(ls.getItem('prop3')).to.be.null |
|
||||
}) |
|
||||
|
|
||||
// Clear key matching string in Local Storage
|
|
||||
cy.get('.ls-btn').click().should(() => { |
|
||||
expect(localStorage.getItem('prop1')).to.eq('red') |
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue') |
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta') |
|
||||
}) |
|
||||
|
|
||||
cy.clearLocalStorage('prop1').should((ls) => { |
|
||||
expect(ls.getItem('prop1')).to.be.null |
|
||||
expect(ls.getItem('prop2')).to.eq('blue') |
|
||||
expect(ls.getItem('prop3')).to.eq('magenta') |
|
||||
}) |
|
||||
|
|
||||
// Clear keys matching regex in Local Storage
|
|
||||
cy.get('.ls-btn').click().should(() => { |
|
||||
expect(localStorage.getItem('prop1')).to.eq('red') |
|
||||
expect(localStorage.getItem('prop2')).to.eq('blue') |
|
||||
expect(localStorage.getItem('prop3')).to.eq('magenta') |
|
||||
}) |
|
||||
|
|
||||
cy.clearLocalStorage(/prop1|2/).should((ls) => { |
|
||||
expect(ls.getItem('prop1')).to.be.null |
|
||||
expect(ls.getItem('prop2')).to.be.null |
|
||||
expect(ls.getItem('prop3')).to.eq('magenta') |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,32 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Location', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/location') |
|
||||
}) |
|
||||
|
|
||||
it('cy.hash() - get the current URL hash', () => { |
|
||||
// https://on.cypress.io/hash
|
|
||||
cy.hash().should('be.empty') |
|
||||
}) |
|
||||
|
|
||||
it('cy.location() - get window.location', () => { |
|
||||
// https://on.cypress.io/location
|
|
||||
cy.location().should((location) => { |
|
||||
expect(location.hash).to.be.empty |
|
||||
expect(location.href).to.eq('https://example.cypress.io/commands/location') |
|
||||
expect(location.host).to.eq('example.cypress.io') |
|
||||
expect(location.hostname).to.eq('example.cypress.io') |
|
||||
expect(location.origin).to.eq('https://example.cypress.io') |
|
||||
expect(location.pathname).to.eq('/commands/location') |
|
||||
expect(location.port).to.eq('') |
|
||||
expect(location.protocol).to.eq('https:') |
|
||||
expect(location.search).to.be.empty |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.url() - get the current URL', () => { |
|
||||
// https://on.cypress.io/url
|
|
||||
cy.url().should('eq', 'https://example.cypress.io/commands/location') |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,104 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Misc', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/misc') |
|
||||
}) |
|
||||
|
|
||||
it('.end() - end the command chain', () => { |
|
||||
// https://on.cypress.io/end
|
|
||||
|
|
||||
// cy.end is useful when you want to end a chain of commands
|
|
||||
// and force Cypress to re-query from the root element
|
|
||||
cy.get('.misc-table').within(() => { |
|
||||
// ends the current chain and yields null
|
|
||||
cy.contains('Cheryl').click().end() |
|
||||
|
|
||||
// queries the entire table again
|
|
||||
cy.contains('Charles').click() |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.exec() - execute a system command', () => { |
|
||||
// execute a system command.
|
|
||||
// so you can take actions necessary for
|
|
||||
// your test outside the scope of Cypress.
|
|
||||
// https://on.cypress.io/exec
|
|
||||
|
|
||||
// we can use Cypress.platform string to
|
|
||||
// select appropriate command
|
|
||||
// https://on.cypress/io/platform
|
|
||||
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`) |
|
||||
|
|
||||
// on CircleCI Windows build machines we have a failure to run bash shell
|
|
||||
// https://github.com/cypress-io/cypress/issues/5169
|
|
||||
// so skip some of the tests by passing flag "--env circle=true"
|
|
||||
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle') |
|
||||
|
|
||||
if (isCircleOnWindows) { |
|
||||
cy.log('Skipping test on CircleCI') |
|
||||
|
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// cy.exec problem on Shippable CI
|
|
||||
// https://github.com/cypress-io/cypress/issues/6718
|
|
||||
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable') |
|
||||
|
|
||||
if (isShippable) { |
|
||||
cy.log('Skipping test on ShippableCI') |
|
||||
|
|
||||
return |
|
||||
} |
|
||||
|
|
||||
cy.exec('echo Jane Lane') |
|
||||
.its('stdout').should('contain', 'Jane Lane') |
|
||||
|
|
||||
if (Cypress.platform === 'win32') { |
|
||||
cy.exec('print cypress.json') |
|
||||
.its('stderr').should('be.empty') |
|
||||
} else { |
|
||||
cy.exec('cat cypress.json') |
|
||||
.its('stderr').should('be.empty') |
|
||||
|
|
||||
cy.exec('pwd') |
|
||||
.its('code').should('eq', 0) |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
it('cy.focused() - get the DOM element that has focus', () => { |
|
||||
// https://on.cypress.io/focused
|
|
||||
cy.get('.misc-form').find('#name').click() |
|
||||
cy.focused().should('have.id', 'name') |
|
||||
|
|
||||
cy.get('.misc-form').find('#description').click() |
|
||||
cy.focused().should('have.id', 'description') |
|
||||
}) |
|
||||
|
|
||||
context('Cypress.Screenshot', function () { |
|
||||
it('cy.screenshot() - take a screenshot', () => { |
|
||||
// https://on.cypress.io/screenshot
|
|
||||
cy.screenshot('my-image') |
|
||||
}) |
|
||||
|
|
||||
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () { |
|
||||
Cypress.Screenshot.defaults({ |
|
||||
blackout: ['.foo'], |
|
||||
capture: 'viewport', |
|
||||
clip: { x: 0, y: 0, width: 200, height: 200 }, |
|
||||
scale: false, |
|
||||
disableTimersAndAnimations: true, |
|
||||
screenshotOnRunFailure: true, |
|
||||
onBeforeScreenshot () { }, |
|
||||
onAfterScreenshot () { }, |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.wrap() - wrap an object', () => { |
|
||||
// https://on.cypress.io/wrap
|
|
||||
cy.wrap({ foo: 'bar' }) |
|
||||
.should('have.property', 'foo') |
|
||||
.and('include', 'bar') |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,56 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Navigation', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io') |
|
||||
cy.get('.navbar-nav').contains('Commands').click() |
|
||||
cy.get('.dropdown-menu').contains('Navigation').click() |
|
||||
}) |
|
||||
|
|
||||
it('cy.go() - go back or forward in the browser\'s history', () => { |
|
||||
// https://on.cypress.io/go
|
|
||||
|
|
||||
cy.location('pathname').should('include', 'navigation') |
|
||||
|
|
||||
cy.go('back') |
|
||||
cy.location('pathname').should('not.include', 'navigation') |
|
||||
|
|
||||
cy.go('forward') |
|
||||
cy.location('pathname').should('include', 'navigation') |
|
||||
|
|
||||
// clicking back
|
|
||||
cy.go(-1) |
|
||||
cy.location('pathname').should('not.include', 'navigation') |
|
||||
|
|
||||
// clicking forward
|
|
||||
cy.go(1) |
|
||||
cy.location('pathname').should('include', 'navigation') |
|
||||
}) |
|
||||
|
|
||||
it('cy.reload() - reload the page', () => { |
|
||||
// https://on.cypress.io/reload
|
|
||||
cy.reload() |
|
||||
|
|
||||
// reload the page without using the cache
|
|
||||
cy.reload(true) |
|
||||
}) |
|
||||
|
|
||||
it('cy.visit() - visit a remote url', () => { |
|
||||
// https://on.cypress.io/visit
|
|
||||
|
|
||||
// Visit any sub-domain of your current domain
|
|
||||
|
|
||||
// Pass options to the visit
|
|
||||
cy.visit('https://example.cypress.io/commands/navigation', { |
|
||||
timeout: 50000, // increase total time for the visit to resolve
|
|
||||
onBeforeLoad (contentWindow) { |
|
||||
// contentWindow is the remote page's window object
|
|
||||
expect(typeof contentWindow === 'object').to.be.true |
|
||||
}, |
|
||||
onLoad (contentWindow) { |
|
||||
// contentWindow is the remote page's window object
|
|
||||
expect(typeof contentWindow === 'object').to.be.true |
|
||||
}, |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,205 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Network Requests', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/network-requests') |
|
||||
}) |
|
||||
|
|
||||
// Manage AJAX / XHR requests in your app
|
|
||||
|
|
||||
it('cy.server() - control behavior of network requests and responses', () => { |
|
||||
// https://on.cypress.io/server
|
|
||||
|
|
||||
cy.server().should((server) => { |
|
||||
// the default options on server
|
|
||||
// you can override any of these options
|
|
||||
expect(server.delay).to.eq(0) |
|
||||
expect(server.method).to.eq('GET') |
|
||||
expect(server.status).to.eq(200) |
|
||||
expect(server.headers).to.be.null |
|
||||
expect(server.response).to.be.null |
|
||||
expect(server.onRequest).to.be.undefined |
|
||||
expect(server.onResponse).to.be.undefined |
|
||||
expect(server.onAbort).to.be.undefined |
|
||||
|
|
||||
// These options control the server behavior
|
|
||||
// affecting all requests
|
|
||||
|
|
||||
// pass false to disable existing route stubs
|
|
||||
expect(server.enable).to.be.true |
|
||||
// forces requests that don't match your routes to 404
|
|
||||
expect(server.force404).to.be.false |
|
||||
|
|
||||
if (Number(Cypress.version.charAt(0)) >= 5) { |
|
||||
// ignores requests from ever being logged or stubbed
|
|
||||
// @ts-ignore
|
|
||||
expect(server.ignore).to.be.a('function') |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
cy.server({ |
|
||||
method: 'POST', |
|
||||
delay: 1000, |
|
||||
status: 422, |
|
||||
response: {}, |
|
||||
}) |
|
||||
|
|
||||
// any route commands will now inherit the above options
|
|
||||
// from the server. anything we pass specifically
|
|
||||
// to route will override the defaults though.
|
|
||||
}) |
|
||||
|
|
||||
it('cy.request() - make an XHR request', () => { |
|
||||
// https://on.cypress.io/request
|
|
||||
cy.request('https://jsonplaceholder.cypress.io/comments') |
|
||||
.should((response) => { |
|
||||
expect(response.status).to.eq(200) |
|
||||
// the server sometimes gets an extra comment posted from another machine
|
|
||||
// which gets returned as 1 extra object
|
|
||||
expect(response.body).to.have.property('length').and.be.oneOf([500, 501]) |
|
||||
expect(response).to.have.property('headers') |
|
||||
expect(response).to.have.property('duration') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.request() - verify response using BDD syntax', () => { |
|
||||
cy.request('https://jsonplaceholder.cypress.io/comments') |
|
||||
.then((response) => { |
|
||||
// https://on.cypress.io/assertions
|
|
||||
expect(response).property('status').to.equal(200) |
|
||||
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501]) |
|
||||
expect(response).to.include.keys('headers', 'duration') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.request() with query parameters', () => { |
|
||||
// will execute request
|
|
||||
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
|
||||
cy.request({ |
|
||||
url: 'https://jsonplaceholder.cypress.io/comments', |
|
||||
qs: { |
|
||||
postId: 1, |
|
||||
id: 3, |
|
||||
}, |
|
||||
}) |
|
||||
.its('body') |
|
||||
.should('be.an', 'array') |
|
||||
.and('have.length', 1) |
|
||||
.its('0') // yields first element of the array
|
|
||||
.should('contain', { |
|
||||
postId: 1, |
|
||||
id: 3, |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.request() - pass result to the second request', () => { |
|
||||
// first, let's find out the userId of the first user we have
|
|
||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') |
|
||||
.its('body') // yields the response object
|
|
||||
.its('0') // yields the first element of the returned list
|
|
||||
// the above two commands its('body').its('0')
|
|
||||
// can be written as its('body.0')
|
|
||||
// if you do not care about TypeScript checks
|
|
||||
.then((user) => { |
|
||||
expect(user).property('id').to.be.a('number') |
|
||||
// make a new post on behalf of the user
|
|
||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { |
|
||||
userId: user.id, |
|
||||
title: 'Cypress Test Runner', |
|
||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.', |
|
||||
}) |
|
||||
}) |
|
||||
// note that the value here is the returned value of the 2nd request
|
|
||||
// which is the new post object
|
|
||||
.then((response) => { |
|
||||
expect(response).property('status').to.equal(201) // new entity created
|
|
||||
expect(response).property('body').to.contain({ |
|
||||
title: 'Cypress Test Runner', |
|
||||
}) |
|
||||
|
|
||||
// we don't know the exact post id - only that it will be > 100
|
|
||||
// since JSONPlaceholder has built-in 100 posts
|
|
||||
expect(response.body).property('id').to.be.a('number') |
|
||||
.and.to.be.gt(100) |
|
||||
|
|
||||
// we don't know the user id here - since it was in above closure
|
|
||||
// so in this test just confirm that the property is there
|
|
||||
expect(response.body).property('userId').to.be.a('number') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.request() - save response in the shared test context', () => { |
|
||||
// https://on.cypress.io/variables-and-aliases
|
|
||||
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1') |
|
||||
.its('body').its('0') // yields the first element of the returned list
|
|
||||
.as('user') // saves the object in the test context
|
|
||||
.then(function () { |
|
||||
// NOTE 👀
|
|
||||
// By the time this callback runs the "as('user')" command
|
|
||||
// has saved the user object in the test context.
|
|
||||
// To access the test context we need to use
|
|
||||
// the "function () { ... }" callback form,
|
|
||||
// otherwise "this" points at a wrong or undefined object!
|
|
||||
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', { |
|
||||
userId: this.user.id, |
|
||||
title: 'Cypress Test Runner', |
|
||||
body: 'Fast, easy and reliable testing for anything that runs in a browser.', |
|
||||
}) |
|
||||
.its('body').as('post') // save the new post from the response
|
|
||||
}) |
|
||||
.then(function () { |
|
||||
// When this callback runs, both "cy.request" API commands have finished
|
|
||||
// and the test context has "user" and "post" objects set.
|
|
||||
// Let's verify them.
|
|
||||
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.route() - route responses to matching requests', () => { |
|
||||
// https://on.cypress.io/route
|
|
||||
|
|
||||
let message = 'whoa, this comment does not exist' |
|
||||
|
|
||||
cy.server() |
|
||||
|
|
||||
// Listen to GET to comments/1
|
|
||||
cy.route('GET', 'comments/*').as('getComment') |
|
||||
|
|
||||
// we have code that gets a comment when
|
|
||||
// the button is clicked in scripts.js
|
|
||||
cy.get('.network-btn').click() |
|
||||
|
|
||||
// https://on.cypress.io/wait
|
|
||||
cy.wait('@getComment').its('status').should('eq', 200) |
|
||||
|
|
||||
// Listen to POST to comments
|
|
||||
cy.route('POST', '/comments').as('postComment') |
|
||||
|
|
||||
// we have code that posts a comment when
|
|
||||
// the button is clicked in scripts.js
|
|
||||
cy.get('.network-post').click() |
|
||||
cy.wait('@postComment').should((xhr) => { |
|
||||
expect(xhr.requestBody).to.include('email') |
|
||||
expect(xhr.requestHeaders).to.have.property('Content-Type') |
|
||||
expect(xhr.responseBody).to.have.property('name', 'Using POST in cy.route()') |
|
||||
}) |
|
||||
|
|
||||
// Stub a response to PUT comments/ ****
|
|
||||
cy.route({ |
|
||||
method: 'PUT', |
|
||||
url: 'comments/*', |
|
||||
status: 404, |
|
||||
response: { error: message }, |
|
||||
delay: 500, |
|
||||
}).as('putComment') |
|
||||
|
|
||||
// we have code that puts a comment when
|
|
||||
// the button is clicked in scripts.js
|
|
||||
cy.get('.network-put').click() |
|
||||
|
|
||||
cy.wait('@putComment') |
|
||||
|
|
||||
// our 404 statusCode logic in scripts.js executed
|
|
||||
cy.get('.network-put-comment').should('contain', message) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,114 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Querying', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/querying') |
|
||||
}) |
|
||||
|
|
||||
// The most commonly used query is 'cy.get()', you can
|
|
||||
// think of this like the '$' in jQuery
|
|
||||
|
|
||||
it('cy.get() - query DOM elements', () => { |
|
||||
// https://on.cypress.io/get
|
|
||||
|
|
||||
cy.get('#query-btn').should('contain', 'Button') |
|
||||
|
|
||||
cy.get('.query-btn').should('contain', 'Button') |
|
||||
|
|
||||
cy.get('#querying .well>button:first').should('contain', 'Button') |
|
||||
// ↲
|
|
||||
// Use CSS selectors just like jQuery
|
|
||||
|
|
||||
cy.get('[data-test-id="test-example"]').should('have.class', 'example') |
|
||||
|
|
||||
// 'cy.get()' yields jQuery object, you can get its attribute
|
|
||||
// by invoking `.attr()` method
|
|
||||
cy.get('[data-test-id="test-example"]') |
|
||||
.invoke('attr', 'data-test-id') |
|
||||
.should('equal', 'test-example') |
|
||||
|
|
||||
// or you can get element's CSS property
|
|
||||
cy.get('[data-test-id="test-example"]') |
|
||||
.invoke('css', 'position') |
|
||||
.should('equal', 'static') |
|
||||
|
|
||||
// or use assertions directly during 'cy.get()'
|
|
||||
// https://on.cypress.io/assertions
|
|
||||
cy.get('[data-test-id="test-example"]') |
|
||||
.should('have.attr', 'data-test-id', 'test-example') |
|
||||
.and('have.css', 'position', 'static') |
|
||||
}) |
|
||||
|
|
||||
it('cy.contains() - query DOM elements with matching content', () => { |
|
||||
// https://on.cypress.io/contains
|
|
||||
cy.get('.query-list') |
|
||||
.contains('bananas') |
|
||||
.should('have.class', 'third') |
|
||||
|
|
||||
// we can pass a regexp to `.contains()`
|
|
||||
cy.get('.query-list') |
|
||||
.contains(/^b\w+/) |
|
||||
.should('have.class', 'third') |
|
||||
|
|
||||
cy.get('.query-list') |
|
||||
.contains('apples') |
|
||||
.should('have.class', 'first') |
|
||||
|
|
||||
// passing a selector to contains will
|
|
||||
// yield the selector containing the text
|
|
||||
cy.get('#querying') |
|
||||
.contains('ul', 'oranges') |
|
||||
.should('have.class', 'query-list') |
|
||||
|
|
||||
cy.get('.query-button') |
|
||||
.contains('Save Form') |
|
||||
.should('have.class', 'btn') |
|
||||
}) |
|
||||
|
|
||||
it('.within() - query DOM elements within a specific element', () => { |
|
||||
// https://on.cypress.io/within
|
|
||||
cy.get('.query-form').within(() => { |
|
||||
cy.get('input:first').should('have.attr', 'placeholder', 'Email') |
|
||||
cy.get('input:last').should('have.attr', 'placeholder', 'Password') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('cy.root() - query the root DOM element', () => { |
|
||||
// https://on.cypress.io/root
|
|
||||
|
|
||||
// By default, root is the document
|
|
||||
cy.root().should('match', 'html') |
|
||||
|
|
||||
cy.get('.query-ul').within(() => { |
|
||||
// In this within, the root is now the ul DOM element
|
|
||||
cy.root().should('have.class', 'query-ul') |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('best practices - selecting elements', () => { |
|
||||
// https://on.cypress.io/best-practices#Selecting-Elements
|
|
||||
cy.get('[data-cy=best-practices-selecting-elements]').within(() => { |
|
||||
// Worst - too generic, no context
|
|
||||
cy.get('button').click() |
|
||||
|
|
||||
// Bad. Coupled to styling. Highly subject to change.
|
|
||||
cy.get('.btn.btn-large').click() |
|
||||
|
|
||||
// Average. Coupled to the `name` attribute which has HTML semantics.
|
|
||||
cy.get('[name=submission]').click() |
|
||||
|
|
||||
// Better. But still coupled to styling or JS event listeners.
|
|
||||
cy.get('#main').click() |
|
||||
|
|
||||
// Slightly better. Uses an ID but also ensures the element
|
|
||||
// has an ARIA role attribute
|
|
||||
cy.get('#main[role=button]').click() |
|
||||
|
|
||||
// Much better. But still coupled to text content that may change.
|
|
||||
cy.contains('Submit').click() |
|
||||
|
|
||||
// Best. Insulated from all changes.
|
|
||||
cy.get('[data-cy=submit]').click() |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,205 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
// remove no check once Cypress.sinon is typed
|
|
||||
// https://github.com/cypress-io/cypress/issues/6720
|
|
||||
|
|
||||
context('Spies, Stubs, and Clock', () => { |
|
||||
it('cy.spy() - wrap a method in a spy', () => { |
|
||||
// https://on.cypress.io/spy
|
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|
||||
|
|
||||
const obj = { |
|
||||
foo () {}, |
|
||||
} |
|
||||
|
|
||||
const spy = cy.spy(obj, 'foo').as('anyArgs') |
|
||||
|
|
||||
obj.foo() |
|
||||
|
|
||||
expect(spy).to.be.called |
|
||||
}) |
|
||||
|
|
||||
it('cy.spy() retries until assertions pass', () => { |
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|
||||
|
|
||||
const obj = { |
|
||||
/** |
|
||||
* Prints the argument passed |
|
||||
* @param x {any} |
|
||||
*/ |
|
||||
foo (x) { |
|
||||
console.log('obj.foo called with', x) |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
cy.spy(obj, 'foo').as('foo') |
|
||||
|
|
||||
setTimeout(() => { |
|
||||
obj.foo('first') |
|
||||
}, 500) |
|
||||
|
|
||||
setTimeout(() => { |
|
||||
obj.foo('second') |
|
||||
}, 2500) |
|
||||
|
|
||||
cy.get('@foo').should('have.been.calledTwice') |
|
||||
}) |
|
||||
|
|
||||
it('cy.stub() - create a stub and/or replace a function with stub', () => { |
|
||||
// https://on.cypress.io/stub
|
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|
||||
|
|
||||
const obj = { |
|
||||
/** |
|
||||
* prints both arguments to the console |
|
||||
* @param a {string} |
|
||||
* @param b {string} |
|
||||
*/ |
|
||||
foo (a, b) { |
|
||||
console.log('a', a, 'b', b) |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
const stub = cy.stub(obj, 'foo').as('foo') |
|
||||
|
|
||||
obj.foo('foo', 'bar') |
|
||||
|
|
||||
expect(stub).to.be.called |
|
||||
}) |
|
||||
|
|
||||
it('cy.clock() - control time in the browser', () => { |
|
||||
// https://on.cypress.io/clock
|
|
||||
|
|
||||
// create the date in UTC so its always the same
|
|
||||
// no matter what local timezone the browser is running in
|
|
||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime() |
|
||||
|
|
||||
cy.clock(now) |
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|
||||
cy.get('#clock-div').click() |
|
||||
.should('have.text', '1489449600') |
|
||||
}) |
|
||||
|
|
||||
it('cy.tick() - move time in the browser', () => { |
|
||||
// https://on.cypress.io/tick
|
|
||||
|
|
||||
// create the date in UTC so its always the same
|
|
||||
// no matter what local timezone the browser is running in
|
|
||||
const now = new Date(Date.UTC(2017, 2, 14)).getTime() |
|
||||
|
|
||||
cy.clock(now) |
|
||||
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks') |
|
||||
cy.get('#tick-div').click() |
|
||||
.should('have.text', '1489449600') |
|
||||
|
|
||||
cy.tick(10000) // 10 seconds passed
|
|
||||
cy.get('#tick-div').click() |
|
||||
.should('have.text', '1489449610') |
|
||||
}) |
|
||||
|
|
||||
it('cy.stub() matches depending on arguments', () => { |
|
||||
// see all possible matchers at
|
|
||||
// https://sinonjs.org/releases/latest/matchers/
|
|
||||
const greeter = { |
|
||||
/** |
|
||||
* Greets a person |
|
||||
* @param {string} name |
|
||||
*/ |
|
||||
greet (name) { |
|
||||
return `Hello, ${name}!` |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
cy.stub(greeter, 'greet') |
|
||||
.callThrough() // if you want non-matched calls to call the real method
|
|
||||
.withArgs(Cypress.sinon.match.string).returns('Hi') |
|
||||
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name')) |
|
||||
|
|
||||
expect(greeter.greet('World')).to.equal('Hi') |
|
||||
// @ts-ignore
|
|
||||
expect(() => greeter.greet(42)).to.throw('Invalid name') |
|
||||
expect(greeter.greet).to.have.been.calledTwice |
|
||||
|
|
||||
// non-matched calls goes the actual method
|
|
||||
// @ts-ignore
|
|
||||
expect(greeter.greet()).to.equal('Hello, undefined!') |
|
||||
}) |
|
||||
|
|
||||
it('matches call arguments using Sinon matchers', () => { |
|
||||
// see all possible matchers at
|
|
||||
// https://sinonjs.org/releases/latest/matchers/
|
|
||||
const calculator = { |
|
||||
/** |
|
||||
* returns the sum of two arguments |
|
||||
* @param a {number} |
|
||||
* @param b {number} |
|
||||
*/ |
|
||||
add (a, b) { |
|
||||
return a + b |
|
||||
}, |
|
||||
} |
|
||||
|
|
||||
const spy = cy.spy(calculator, 'add').as('add') |
|
||||
|
|
||||
expect(calculator.add(2, 3)).to.equal(5) |
|
||||
|
|
||||
// if we want to assert the exact values used during the call
|
|
||||
expect(spy).to.be.calledWith(2, 3) |
|
||||
|
|
||||
// let's confirm "add" method was called with two numbers
|
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number) |
|
||||
|
|
||||
// alternatively, provide the value to match
|
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3)) |
|
||||
|
|
||||
// match any value
|
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3) |
|
||||
|
|
||||
// match any value from a list
|
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3) |
|
||||
|
|
||||
/** |
|
||||
* Returns true if the given number is event |
|
||||
* @param {number} x |
|
||||
*/ |
|
||||
const isEven = (x) => x % 2 === 0 |
|
||||
|
|
||||
// expect the value to pass a custom predicate function
|
|
||||
// the second argument to "sinon.match(predicate, message)" is
|
|
||||
// shown if the predicate does not pass and assertion fails
|
|
||||
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3) |
|
||||
|
|
||||
/** |
|
||||
* Returns a function that checks if a given number is larger than the limit |
|
||||
* @param {number} limit |
|
||||
* @returns {(x: number) => boolean} |
|
||||
*/ |
|
||||
const isGreaterThan = (limit) => (x) => x > limit |
|
||||
|
|
||||
/** |
|
||||
* Returns a function that checks if a given number is less than the limit |
|
||||
* @param {number} limit |
|
||||
* @returns {(x: number) => boolean} |
|
||||
*/ |
|
||||
const isLessThan = (limit) => (x) => x < limit |
|
||||
|
|
||||
// you can combine several matchers using "and", "or"
|
|
||||
expect(spy).to.be.calledWith( |
|
||||
Cypress.sinon.match.number, |
|
||||
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')), |
|
||||
) |
|
||||
|
|
||||
expect(spy).to.be.calledWith( |
|
||||
Cypress.sinon.match.number, |
|
||||
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)), |
|
||||
) |
|
||||
|
|
||||
// matchers can be used from BDD assertions
|
|
||||
cy.get('@add').should('have.been.calledWith', |
|
||||
Cypress.sinon.match.number, Cypress.sinon.match(3)) |
|
||||
|
|
||||
// you can alias matchers for shorter test code
|
|
||||
const { match: M } = Cypress.sinon |
|
||||
|
|
||||
cy.get('@add').should('have.been.calledWith', M.number, M(3)) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,121 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Traversal', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/traversal') |
|
||||
}) |
|
||||
|
|
||||
it('.children() - get child DOM elements', () => { |
|
||||
// https://on.cypress.io/children
|
|
||||
cy.get('.traversal-breadcrumb') |
|
||||
.children('.active') |
|
||||
.should('contain', 'Data') |
|
||||
}) |
|
||||
|
|
||||
it('.closest() - get closest ancestor DOM element', () => { |
|
||||
// https://on.cypress.io/closest
|
|
||||
cy.get('.traversal-badge') |
|
||||
.closest('ul') |
|
||||
.should('have.class', 'list-group') |
|
||||
}) |
|
||||
|
|
||||
it('.eq() - get a DOM element at a specific index', () => { |
|
||||
// https://on.cypress.io/eq
|
|
||||
cy.get('.traversal-list>li') |
|
||||
.eq(1).should('contain', 'siamese') |
|
||||
}) |
|
||||
|
|
||||
it('.filter() - get DOM elements that match the selector', () => { |
|
||||
// https://on.cypress.io/filter
|
|
||||
cy.get('.traversal-nav>li') |
|
||||
.filter('.active').should('contain', 'About') |
|
||||
}) |
|
||||
|
|
||||
it('.find() - get descendant DOM elements of the selector', () => { |
|
||||
// https://on.cypress.io/find
|
|
||||
cy.get('.traversal-pagination') |
|
||||
.find('li').find('a') |
|
||||
.should('have.length', 7) |
|
||||
}) |
|
||||
|
|
||||
it('.first() - get first DOM element', () => { |
|
||||
// https://on.cypress.io/first
|
|
||||
cy.get('.traversal-table td') |
|
||||
.first().should('contain', '1') |
|
||||
}) |
|
||||
|
|
||||
it('.last() - get last DOM element', () => { |
|
||||
// https://on.cypress.io/last
|
|
||||
cy.get('.traversal-buttons .btn') |
|
||||
.last().should('contain', 'Submit') |
|
||||
}) |
|
||||
|
|
||||
it('.next() - get next sibling DOM element', () => { |
|
||||
// https://on.cypress.io/next
|
|
||||
cy.get('.traversal-ul') |
|
||||
.contains('apples').next().should('contain', 'oranges') |
|
||||
}) |
|
||||
|
|
||||
it('.nextAll() - get all next sibling DOM elements', () => { |
|
||||
// https://on.cypress.io/nextall
|
|
||||
cy.get('.traversal-next-all') |
|
||||
.contains('oranges') |
|
||||
.nextAll().should('have.length', 3) |
|
||||
}) |
|
||||
|
|
||||
it('.nextUntil() - get next sibling DOM elements until next el', () => { |
|
||||
// https://on.cypress.io/nextuntil
|
|
||||
cy.get('#veggies') |
|
||||
.nextUntil('#nuts').should('have.length', 3) |
|
||||
}) |
|
||||
|
|
||||
it('.not() - remove DOM elements from set of DOM elements', () => { |
|
||||
// https://on.cypress.io/not
|
|
||||
cy.get('.traversal-disabled .btn') |
|
||||
.not('[disabled]').should('not.contain', 'Disabled') |
|
||||
}) |
|
||||
|
|
||||
it('.parent() - get parent DOM element from DOM elements', () => { |
|
||||
// https://on.cypress.io/parent
|
|
||||
cy.get('.traversal-mark') |
|
||||
.parent().should('contain', 'Morbi leo risus') |
|
||||
}) |
|
||||
|
|
||||
it('.parents() - get parent DOM elements from DOM elements', () => { |
|
||||
// https://on.cypress.io/parents
|
|
||||
cy.get('.traversal-cite') |
|
||||
.parents().should('match', 'blockquote') |
|
||||
}) |
|
||||
|
|
||||
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => { |
|
||||
// https://on.cypress.io/parentsuntil
|
|
||||
cy.get('.clothes-nav') |
|
||||
.find('.active') |
|
||||
.parentsUntil('.clothes-nav') |
|
||||
.should('have.length', 2) |
|
||||
}) |
|
||||
|
|
||||
it('.prev() - get previous sibling DOM element', () => { |
|
||||
// https://on.cypress.io/prev
|
|
||||
cy.get('.birds').find('.active') |
|
||||
.prev().should('contain', 'Lorikeets') |
|
||||
}) |
|
||||
|
|
||||
it('.prevAll() - get all previous sibling DOM elements', () => { |
|
||||
// https://on.cypress.io/prevAll
|
|
||||
cy.get('.fruits-list').find('.third') |
|
||||
.prevAll().should('have.length', 2) |
|
||||
}) |
|
||||
|
|
||||
it('.prevUntil() - get all previous sibling DOM elements until el', () => { |
|
||||
// https://on.cypress.io/prevUntil
|
|
||||
cy.get('.foods-list').find('#nuts') |
|
||||
.prevUntil('#veggies').should('have.length', 3) |
|
||||
}) |
|
||||
|
|
||||
it('.siblings() - get all sibling DOM elements', () => { |
|
||||
// https://on.cypress.io/siblings
|
|
||||
cy.get('.traversal-pills .active') |
|
||||
.siblings().should('have.length', 2) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,136 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Utilities', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/utilities') |
|
||||
}) |
|
||||
|
|
||||
it('Cypress._ - call a lodash method', () => { |
|
||||
// https://on.cypress.io/_
|
|
||||
cy.request('https://jsonplaceholder.cypress.io/users') |
|
||||
.then((response) => { |
|
||||
let ids = Cypress._.chain(response.body).map('id').take(3).value() |
|
||||
|
|
||||
expect(ids).to.deep.eq([1, 2, 3]) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('Cypress.$ - call a jQuery method', () => { |
|
||||
// https://on.cypress.io/$
|
|
||||
let $li = Cypress.$('.utility-jquery li:first') |
|
||||
|
|
||||
cy.wrap($li) |
|
||||
.should('not.have.class', 'active') |
|
||||
.click() |
|
||||
.should('have.class', 'active') |
|
||||
}) |
|
||||
|
|
||||
it('Cypress.Blob - blob utilities and base64 string conversion', () => { |
|
||||
// https://on.cypress.io/blob
|
|
||||
cy.get('.utility-blob').then(($div) => { |
|
||||
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
|
||||
// get the dataUrl string for the javascript-logo
|
|
||||
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous') |
|
||||
.then((dataUrl) => { |
|
||||
// create an <img> element and set its src to the dataUrl
|
|
||||
let img = Cypress.$('<img />', { src: dataUrl }) |
|
||||
|
|
||||
// need to explicitly return cy here since we are initially returning
|
|
||||
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
|
||||
// append the image
|
|
||||
$div.append(img) |
|
||||
|
|
||||
cy.get('.utility-blob img').click() |
|
||||
.should('have.attr', 'src', dataUrl) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('Cypress.minimatch - test out glob patterns against strings', () => { |
|
||||
// https://on.cypress.io/minimatch
|
|
||||
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', { |
|
||||
matchBase: true, |
|
||||
}) |
|
||||
|
|
||||
expect(matching, 'matching wildcard').to.be.true |
|
||||
|
|
||||
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', { |
|
||||
matchBase: true, |
|
||||
}) |
|
||||
|
|
||||
expect(matching, 'comments').to.be.false |
|
||||
|
|
||||
// ** matches against all downstream path segments
|
|
||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', { |
|
||||
matchBase: true, |
|
||||
}) |
|
||||
|
|
||||
expect(matching, 'comments').to.be.true |
|
||||
|
|
||||
// whereas * matches only the next path segment
|
|
||||
|
|
||||
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', { |
|
||||
matchBase: false, |
|
||||
}) |
|
||||
|
|
||||
expect(matching, 'comments').to.be.false |
|
||||
}) |
|
||||
|
|
||||
it('Cypress.moment() - format or parse dates using a moment method', () => { |
|
||||
// https://on.cypress.io/moment
|
|
||||
const time = Cypress.moment('2014-04-25T19:38:53.196Z').utc().format('h:mm A') |
|
||||
|
|
||||
expect(time).to.be.a('string') |
|
||||
|
|
||||
cy.get('.utility-moment').contains('3:38 PM') |
|
||||
.should('have.class', 'badge') |
|
||||
|
|
||||
// the time in the element should be between 3pm and 5pm
|
|
||||
const start = Cypress.moment('3:00 PM', 'LT') |
|
||||
const end = Cypress.moment('5:00 PM', 'LT') |
|
||||
|
|
||||
cy.get('.utility-moment .badge') |
|
||||
.should(($el) => { |
|
||||
// parse American time like "3:38 PM"
|
|
||||
const m = Cypress.moment($el.text().trim(), 'LT') |
|
||||
|
|
||||
// display hours + minutes + AM|PM
|
|
||||
const f = 'h:mm A' |
|
||||
|
|
||||
expect(m.isBetween(start, end), |
|
||||
`${m.format(f)} should be between ${start.format(f)} and ${end.format(f)}`).to.be.true |
|
||||
}) |
|
||||
}) |
|
||||
|
|
||||
it('Cypress.Promise - instantiate a bluebird promise', () => { |
|
||||
// https://on.cypress.io/promise
|
|
||||
let waited = false |
|
||||
|
|
||||
/** |
|
||||
* @return Bluebird<string> |
|
||||
*/ |
|
||||
function waitOneSecond () { |
|
||||
// return a promise that resolves after 1 second
|
|
||||
// @ts-ignore TS2351 (new Cypress.Promise)
|
|
||||
return new Cypress.Promise((resolve, reject) => { |
|
||||
setTimeout(() => { |
|
||||
// set waited to true
|
|
||||
waited = true |
|
||||
|
|
||||
// resolve with 'foo' string
|
|
||||
resolve('foo') |
|
||||
}, 1000) |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
cy.then(() => { |
|
||||
// return a promise to cy.then() that
|
|
||||
// is awaited until it resolves
|
|
||||
// @ts-ignore TS7006
|
|
||||
return waitOneSecond().then((str) => { |
|
||||
expect(str).to.eq('foo') |
|
||||
expect(waited).to.be.true |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,59 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Viewport', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/viewport') |
|
||||
}) |
|
||||
|
|
||||
it('cy.viewport() - set the viewport size and dimension', () => { |
|
||||
// https://on.cypress.io/viewport
|
|
||||
|
|
||||
cy.get('#navbar').should('be.visible') |
|
||||
cy.viewport(320, 480) |
|
||||
|
|
||||
// the navbar should have collapse since our screen is smaller
|
|
||||
cy.get('#navbar').should('not.be.visible') |
|
||||
cy.get('.navbar-toggle').should('be.visible').click() |
|
||||
cy.get('.nav').find('a').should('be.visible') |
|
||||
|
|
||||
// lets see what our app looks like on a super large screen
|
|
||||
cy.viewport(2999, 2999) |
|
||||
|
|
||||
// cy.viewport() accepts a set of preset sizes
|
|
||||
// to easily set the screen to a device's width and height
|
|
||||
|
|
||||
// We added a cy.wait() between each viewport change so you can see
|
|
||||
// the change otherwise it is a little too fast to see :)
|
|
||||
|
|
||||
cy.viewport('macbook-15') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('macbook-13') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('macbook-11') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('ipad-2') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('ipad-mini') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('iphone-6+') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('iphone-6') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('iphone-5') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('iphone-4') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('iphone-3') |
|
||||
cy.wait(200) |
|
||||
|
|
||||
// cy.viewport() accepts an orientation for all presets
|
|
||||
// the default orientation is 'portrait'
|
|
||||
cy.viewport('ipad-2', 'portrait') |
|
||||
cy.wait(200) |
|
||||
cy.viewport('iphone-4', 'landscape') |
|
||||
cy.wait(200) |
|
||||
|
|
||||
// The viewport will be reset back to the default dimensions
|
|
||||
// in between tests (the default can be set in cypress.json)
|
|
||||
}) |
|
||||
}) |
|
||||
@ -1,33 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Waiting', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/waiting') |
|
||||
}) |
|
||||
// BE CAREFUL of adding unnecessary wait times.
|
|
||||
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
|
||||
|
|
||||
// https://on.cypress.io/wait
|
|
||||
it('cy.wait() - wait for a specific amount of time', () => { |
|
||||
cy.get('.wait-input1').type('Wait 1000ms after typing') |
|
||||
cy.wait(1000) |
|
||||
cy.get('.wait-input2').type('Wait 1000ms after typing') |
|
||||
cy.wait(1000) |
|
||||
cy.get('.wait-input3').type('Wait 1000ms after typing') |
|
||||
cy.wait(1000) |
|
||||
}) |
|
||||
|
|
||||
it('cy.wait() - wait for a specific route', () => { |
|
||||
cy.server() |
|
||||
|
|
||||
// Listen to GET to comments/1
|
|
||||
cy.route('GET', 'comments/*').as('getComment') |
|
||||
|
|
||||
// we have code that gets a comment when
|
|
||||
// the button is clicked in scripts.js
|
|
||||
cy.get('.network-btn').click() |
|
||||
|
|
||||
// wait for GET comments/1
|
|
||||
cy.wait('@getComment').its('status').should('eq', 200) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,22 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
|
|
||||
context('Window', () => { |
|
||||
beforeEach(() => { |
|
||||
cy.visit('https://example.cypress.io/commands/window') |
|
||||
}) |
|
||||
|
|
||||
it('cy.window() - get the global window object', () => { |
|
||||
// https://on.cypress.io/window
|
|
||||
cy.window().should('have.property', 'top') |
|
||||
}) |
|
||||
|
|
||||
it('cy.document() - get the document object', () => { |
|
||||
// https://on.cypress.io/document
|
|
||||
cy.document().should('have.property', 'charset').and('eq', 'UTF-8') |
|
||||
}) |
|
||||
|
|
||||
it('cy.title() - get the title', () => { |
|
||||
// https://on.cypress.io/title
|
|
||||
cy.title().should('include', 'Kitchen Sink') |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,5 +0,0 @@ |
|||||
{ |
|
||||
"name": "Using fixtures to represent data", |
|
||||
"email": "hello@cypress.io", |
|
||||
"body": "Fixtures are a great way to mock data for responses to routes" |
|
||||
} |
|
||||
@ -1,9 +0,0 @@ |
|||||
// https://docs.cypress.io/api/introduction/api.html
|
|
||||
|
|
||||
describe('Homepage', () => { |
|
||||
it('Shows correct text', () => { |
|
||||
cy.visit('/') |
|
||||
|
|
||||
cy.contains('h1', 'Hello Vue 3 + Vite + Tailwind CSS') |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,21 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
// ***********************************************************
|
|
||||
// This example plugins/index.js can be used to load plugins
|
|
||||
//
|
|
||||
// You can change the location of this file or turn off loading
|
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
|
||||
//
|
|
||||
// You can read more here:
|
|
||||
// https://on.cypress.io/plugins-guide
|
|
||||
// ***********************************************************
|
|
||||
|
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
|
||||
// the project's config changing)
|
|
||||
|
|
||||
/** |
|
||||
* @type {Cypress.PluginConfig} |
|
||||
*/ |
|
||||
module.exports = (on, config) => { |
|
||||
// `on` is used to hook into various events Cypress emits
|
|
||||
// `config` is the resolved Cypress config
|
|
||||
} |
|
||||
@ -1,25 +0,0 @@ |
|||||
// ***********************************************
|
|
||||
// This example commands.js shows you how to
|
|
||||
// create various custom commands and overwrite
|
|
||||
// existing commands.
|
|
||||
//
|
|
||||
// For more comprehensive examples of custom
|
|
||||
// commands please read more here:
|
|
||||
// https://on.cypress.io/custom-commands
|
|
||||
// ***********************************************
|
|
||||
//
|
|
||||
//
|
|
||||
// -- This is a parent command --
|
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
|
||||
//
|
|
||||
//
|
|
||||
// -- This is a child command --
|
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||
//
|
|
||||
//
|
|
||||
// -- This is a dual command --
|
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||
//
|
|
||||
//
|
|
||||
// -- This will overwrite an existing command --
|
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
|
||||
@ -1,34 +0,0 @@ |
|||||
/// <reference types="cypress" />
|
|
||||
// ***********************************************************
|
|
||||
// This example support/index.js is processed and
|
|
||||
// loaded automatically before your test files.
|
|
||||
//
|
|
||||
// This is a great place to put global configuration and
|
|
||||
// behavior that modifies Cypress.
|
|
||||
//
|
|
||||
// You can change the location of this file or turn off
|
|
||||
// automatically serving support files with the
|
|
||||
// 'supportFile' configuration option.
|
|
||||
//
|
|
||||
// You can read more here:
|
|
||||
// https://on.cypress.io/configuration
|
|
||||
// ***********************************************************
|
|
||||
|
|
||||
// Import commands.js using ES2015 syntax:
|
|
||||
import './commands' |
|
||||
|
|
||||
Cypress.on('window:before:load', (win) => { |
|
||||
win.handleFromCypress = function (request) { |
|
||||
return fetch(request.url, { |
|
||||
method: request.method, |
|
||||
headers: request.requestHeaders, |
|
||||
body: request.requestBody, |
|
||||
}).then((res) => { |
|
||||
const content = |
|
||||
res.headers.map['content-type'] === 'application/json' ? res.json() : res.text() |
|
||||
return new Promise((resolve) => { |
|
||||
content.then((body) => resolve([res.status, res.headers, body])) |
|
||||
}) |
|
||||
}) |
|
||||
} |
|
||||
}) |
|
||||
@ -1,16 +1,47 @@ |
|||||
import * as path from 'path' |
import * as path from 'path' |
||||
import { defineConfig } from 'vite' |
import { defineConfig, loadEnv } from 'vite' |
||||
import vue from '@vitejs/plugin-vue' |
import vue from '@vitejs/plugin-vue' |
||||
import pkg from './package.json' |
import pkg from './package.json' |
||||
|
|
||||
export default defineConfig({ |
export default ({ mode }) => { |
||||
plugins: [vue()], |
return defineConfig({ |
||||
resolve: { |
plugins: [vue()], |
||||
alias: { |
server: { |
||||
'@': path.resolve(__dirname, './src'), |
open: true, |
||||
|
proxy: { |
||||
|
// '^/getaway': {
|
||||
|
// target: loadEnv(mode, process.cwd()).VITE_API_URL,
|
||||
|
// changeOrigin: true,
|
||||
|
// rewrite: (path) => path.replace(/^\/getaway/, ''),
|
||||
|
// },
|
||||
|
'/api': { |
||||
|
target: 'http://jsonplaceholder.typicode.com', |
||||
|
changeOrigin: true, |
||||
|
rewrite: (path) => path.replace(/^\/api/, ''), |
||||
|
}, |
||||
|
'/getaway': { |
||||
|
target: 'https://test.tall.wiki/gateway', |
||||
|
changeOrigin: true, |
||||
|
rewrite: (path) => { |
||||
|
console.log('path: ', path) |
||||
|
path.replace(/^\/getaway/, '') |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
}, |
}, |
||||
}, |
resolve: { |
||||
define: { |
alias: { |
||||
_APP_VERSION: JSON.stringify(pkg.version), |
'@': path.resolve(__dirname, './src'), |
||||
}, |
comp: path.resolve(__dirname, './src/components'), |
||||
}) |
views: path.resolve(__dirname, './src/views'), |
||||
|
api: path.resolve(__dirname, './src/api'), |
||||
|
utils: path.resolve(__dirname, './src/utils'), |
||||
|
assets: path.resolve(__dirname, './src/assets'), |
||||
|
store: path.resolve(__dirname, './src/store'), |
||||
|
}, |
||||
|
}, |
||||
|
define: { |
||||
|
_APP_VERSION: JSON.stringify(pkg.version), |
||||
|
}, |
||||
|
}) |
||||
|
} |
||||
|
|||||
File diff suppressed because it is too large
Loading…
Reference in new issue