@jx3box/jx3box-ui 2.3.13 → 2.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/agents/auth-token-refresh.md +13 -0
- package/package.json +1 -1
- package/src/CommonHeader.vue +39 -22
- package/src/filters/clientBy.vue +12 -5
- package/src/header/alternate.vue +11 -5
- package/src/header/userInfo.vue +24 -0
- package/src/stories/filters/FilterGallery.stories.js +1 -0
- package/src/stories/filters/clientBy.stories.js +1 -0
- package/src/utils/auth-token-refresh.js +39 -3
- package/tests/auth-token-refresh.test.js +41 -1
- package/tests/client-by-controlled.test.js +79 -0
|
@@ -41,6 +41,16 @@ PC 公共库存在马甲切换:
|
|
|
41
41
|
|
|
42
42
|
否则用户切换马甲时可能拿回旧 token,或者后续请求继续优先使用旧 `__token`。
|
|
43
43
|
|
|
44
|
+
马甲切换时必须先将目标马甲的 token 同步到 `localStorage.__token`;如果当前标签页存在
|
|
45
|
+
`sessionStorage.__token`,也要一起更新,然后才能调用 refresh 接口。退出登录(包括
|
|
46
|
+
`token_version` 触发的强制退出)时必须同时清理这两个 `__token`。否则业务页可能从普通
|
|
47
|
+
`uid` 读取新账号,而接口仍携带旧账号的高优先级 token,最终在同一页面显示两个 UID。
|
|
48
|
+
|
|
49
|
+
新增马甲跳转登录页前也要先清理两个 `__token`,因为账号页登录成功默认只更新普通
|
|
50
|
+
`token/uid`。公共头拿到 `/user/my/info` 后,如果接口 UID 与本地 UID 不一致、当前又不是
|
|
51
|
+
显式 `?__token=` 入口,应清除遗留覆盖 token 并刷新一次完成自愈。显式 URL token 入口则
|
|
52
|
+
强制执行一次 refresh,把普通用户缓存归一到 URL token 对应的账号;UID 发生变化时刷新页面。
|
|
53
|
+
|
|
44
54
|
## 自动续期开关
|
|
45
55
|
|
|
46
56
|
默认开启。可通过 localStorage 关闭:
|
|
@@ -51,6 +61,9 @@ localStorage.setItem("jx3box:auto_refresh_token", "false");
|
|
|
51
61
|
|
|
52
62
|
值为 `"false"` 或 `"0"` 时不自动续期;其它情况按默认开启处理。
|
|
53
63
|
|
|
64
|
+
该开关只控制后台自动续期。马甲切换和显式 URL token 身份归一属于用户主动登录流程,使用
|
|
65
|
+
`force` 刷新,不受此开关影响。
|
|
66
|
+
|
|
54
67
|
## 失败语义
|
|
55
68
|
|
|
56
69
|
自动续期失败不主动登出,也不阻断公共头渲染。后续业务请求如果发现 token 过期或无权限,仍按宿主项目既有鉴权失败逻辑处理。
|
package/package.json
CHANGED
package/src/CommonHeader.vue
CHANGED
|
@@ -84,7 +84,7 @@ import miniprogram from "@jx3box/jx3box-common/data/miniprogram.json";
|
|
|
84
84
|
// 数据
|
|
85
85
|
import { getGlobalConfig } from "../service/header";
|
|
86
86
|
import { getConfig, getMyAccountStatus } from "../service/cms";
|
|
87
|
-
import { refreshTokenIfNeeded } from "./utils/auth-token-refresh";
|
|
87
|
+
import { clearActiveAuthToken, refreshTokenIfNeeded } from "./utils/auth-token-refresh";
|
|
88
88
|
import User from "@jx3box/jx3box-common/js/user.js";
|
|
89
89
|
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
90
90
|
|
|
@@ -194,7 +194,7 @@ export default {
|
|
|
194
194
|
|
|
195
195
|
if (User.isLogin()) {
|
|
196
196
|
this.loadAsset();
|
|
197
|
-
this.refreshAuthToken();
|
|
197
|
+
this.refreshAuthToken({ force: !!token, reloadOnIdentityChange: !!token });
|
|
198
198
|
this.initAccountReadyState();
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -209,23 +209,27 @@ export default {
|
|
|
209
209
|
// 先保存最新的token_version
|
|
210
210
|
localStorage.setItem("token_version", global_token_version);
|
|
211
211
|
// 然后执行登出操作
|
|
212
|
-
User.destroy()
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
212
|
+
User.destroy()
|
|
213
|
+
.finally(() => {
|
|
214
|
+
clearActiveAuthToken();
|
|
215
|
+
})
|
|
216
|
+
.then(() => {
|
|
217
|
+
this.$refs.user?.logout();
|
|
218
|
+
// 清除马甲所有马甲信息
|
|
219
|
+
let keys = Object.keys(localStorage);
|
|
220
|
+
let alternate = keys.filter((key) => key.startsWith("jx3box-alternate-"));
|
|
221
|
+
|
|
222
|
+
alternate.forEach((key) => {
|
|
223
|
+
localStorage.removeItem(key);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (
|
|
227
|
+
location.pathname.startsWith("/dashboard") ||
|
|
228
|
+
location.pathname.startsWith("/publish")
|
|
229
|
+
) {
|
|
230
|
+
location.href = this.siteRoot;
|
|
231
|
+
}
|
|
220
232
|
});
|
|
221
|
-
|
|
222
|
-
if (
|
|
223
|
-
location.pathname.startsWith("/dashboard") ||
|
|
224
|
-
location.pathname.startsWith("/publish")
|
|
225
|
-
) {
|
|
226
|
-
location.href = this.siteRoot;
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
233
|
}
|
|
230
234
|
} else {
|
|
231
235
|
// 非登录状态也更新token_version,确保用户下次登录时使用新版本
|
|
@@ -236,10 +240,23 @@ export default {
|
|
|
236
240
|
});
|
|
237
241
|
},
|
|
238
242
|
|
|
239
|
-
refreshAuthToken: function () {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
refreshAuthToken: function ({ force = false, reloadOnIdentityChange = false } = {}) {
|
|
244
|
+
const previousUid = User.getInfo()?.uid;
|
|
245
|
+
return refreshTokenIfNeeded({ force })
|
|
246
|
+
.then((refreshed) => {
|
|
247
|
+
if (
|
|
248
|
+
refreshed &&
|
|
249
|
+
reloadOnIdentityChange &&
|
|
250
|
+
String(previousUid || "") !== String(User.getInfo()?.uid || "")
|
|
251
|
+
) {
|
|
252
|
+
location.reload();
|
|
253
|
+
}
|
|
254
|
+
return refreshed;
|
|
255
|
+
})
|
|
256
|
+
.catch(() => {
|
|
257
|
+
// 自动续期失败不影响公共头渲染,后续鉴权请求会按既有逻辑处理登录态。
|
|
258
|
+
return false;
|
|
259
|
+
});
|
|
243
260
|
},
|
|
244
261
|
|
|
245
262
|
handleVisibilityChange: function () {
|
package/src/filters/clientBy.vue
CHANGED
|
@@ -36,6 +36,10 @@ export default {
|
|
|
36
36
|
type: Boolean,
|
|
37
37
|
default: false,
|
|
38
38
|
},
|
|
39
|
+
autoDetect: {
|
|
40
|
+
type: Boolean,
|
|
41
|
+
default: true,
|
|
42
|
+
},
|
|
39
43
|
},
|
|
40
44
|
data: function () {
|
|
41
45
|
return {
|
|
@@ -57,6 +61,11 @@ export default {
|
|
|
57
61
|
return this.clients || clients;
|
|
58
62
|
},
|
|
59
63
|
},
|
|
64
|
+
watch: {
|
|
65
|
+
type: function (value) {
|
|
66
|
+
this.client = value || "";
|
|
67
|
+
},
|
|
68
|
+
},
|
|
60
69
|
methods: {
|
|
61
70
|
filter: function (val) {
|
|
62
71
|
this.client = val;
|
|
@@ -65,11 +74,9 @@ export default {
|
|
|
65
74
|
},
|
|
66
75
|
},
|
|
67
76
|
mounted: function () {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.filter(this.client);
|
|
72
|
-
}
|
|
77
|
+
if (!this.autoDetect) return;
|
|
78
|
+
const client = location.hostname.includes("origin") ? "origin" : "std";
|
|
79
|
+
this.filter(client);
|
|
73
80
|
},
|
|
74
81
|
};
|
|
75
82
|
</script>
|
package/src/header/alternate.vue
CHANGED
|
@@ -47,7 +47,7 @@ import { showAvatar } from "@jx3box/jx3box-common/js/utils";
|
|
|
47
47
|
import dayjs from "dayjs";
|
|
48
48
|
import User from "@jx3box/jx3box-common/js/user";
|
|
49
49
|
import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
|
|
50
|
-
import {
|
|
50
|
+
import { clearActiveAuthToken, refreshTokenIfNeeded, syncActiveAuthToken } from "../utils/auth-token-refresh";
|
|
51
51
|
import i18nMixin from "../../i18n/mixin";
|
|
52
52
|
export default {
|
|
53
53
|
name: "AlternateSwitch",
|
|
@@ -162,8 +162,11 @@ export default {
|
|
|
162
162
|
type: "warning",
|
|
163
163
|
}
|
|
164
164
|
)
|
|
165
|
-
.then(() => {
|
|
166
|
-
User.update(item)
|
|
165
|
+
.then(async () => {
|
|
166
|
+
await User.update(item);
|
|
167
|
+
// 请求层优先读取 __token;必须与刚切换的马甲 token 同步,避免继续请求旧账号。
|
|
168
|
+
syncActiveAuthToken(item.token);
|
|
169
|
+
try {
|
|
167
170
|
localStorage.setItem(
|
|
168
171
|
"jx3box-alternate-" + item.uid,
|
|
169
172
|
JSON.stringify({
|
|
@@ -171,10 +174,11 @@ export default {
|
|
|
171
174
|
created_at: Number(localStorage.getItem("created_at")),
|
|
172
175
|
})
|
|
173
176
|
);
|
|
174
|
-
await
|
|
177
|
+
await refreshTokenIfNeeded({ force: true });
|
|
178
|
+
} finally {
|
|
175
179
|
location.reload();
|
|
176
180
|
this.visible = false;
|
|
177
|
-
}
|
|
181
|
+
}
|
|
178
182
|
})
|
|
179
183
|
.catch(() => {});
|
|
180
184
|
},
|
|
@@ -202,6 +206,8 @@ export default {
|
|
|
202
206
|
this.$message.error(this.$jx3boxT("jx3boxUi.commonHeader.alternate.maxFive", "最多只能添加5个马甲"));
|
|
203
207
|
return;
|
|
204
208
|
}
|
|
209
|
+
// 新账号登录只会写普通 token;先清掉当前账号的高优先级 token,避免登录完成后仍请求旧账号。
|
|
210
|
+
clearActiveAuthToken();
|
|
205
211
|
// 跳转至登录页
|
|
206
212
|
location.href = JX3BOX.__Links.account.login + "?alternate=1";
|
|
207
213
|
},
|
package/src/header/userInfo.vue
CHANGED
|
@@ -129,6 +129,7 @@ import { getMenu } from "../../service/header";
|
|
|
129
129
|
import Bus from "../../utils/bus";
|
|
130
130
|
import alternate from "./alternate.vue";
|
|
131
131
|
import i18nMixin from "../../i18n/mixin";
|
|
132
|
+
import { clearActiveAuthToken, hasActiveAuthTokenOverride } from "../utils/auth-token-refresh";
|
|
132
133
|
|
|
133
134
|
const { __Links, __Root, __imgPath, __OriginRoot } = JX3BOX;
|
|
134
135
|
export default {
|
|
@@ -211,6 +212,9 @@ export default {
|
|
|
211
212
|
showAvatar,
|
|
212
213
|
logout: function (mute = false) {
|
|
213
214
|
User.destroy()
|
|
215
|
+
.finally(() => {
|
|
216
|
+
clearActiveAuthToken();
|
|
217
|
+
})
|
|
214
218
|
.then(() => {
|
|
215
219
|
this.$emit("logout");
|
|
216
220
|
|
|
@@ -239,9 +243,29 @@ export default {
|
|
|
239
243
|
showUserName: function (val) {
|
|
240
244
|
return val || this.$jx3boxT("jx3boxUi.commonHeader.anonymous", "匿名");
|
|
241
245
|
},
|
|
246
|
+
repairMismatchedIdentity: function (data = {}) {
|
|
247
|
+
const cachedUid = Number(User.getInfo()?.uid || 0);
|
|
248
|
+
const requestUid = Number(data.ID || 0);
|
|
249
|
+
const hasUrlToken = new URLSearchParams(location.search).has("__token");
|
|
250
|
+
|
|
251
|
+
if (
|
|
252
|
+
cachedUid &&
|
|
253
|
+
requestUid &&
|
|
254
|
+
cachedUid !== requestUid &&
|
|
255
|
+
hasActiveAuthTokenOverride() &&
|
|
256
|
+
!hasUrlToken
|
|
257
|
+
) {
|
|
258
|
+
clearActiveAuthToken();
|
|
259
|
+
location.reload();
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
},
|
|
242
264
|
loadMyInfo: function () {
|
|
243
265
|
getMyInfo()
|
|
244
266
|
.then((data) => {
|
|
267
|
+
// 普通 uid 与接口身份不一致时,优先清除遗留的高优先级 token 并重新请求一次。
|
|
268
|
+
if (this.repairMismatchedIdentity(data)) return;
|
|
245
269
|
this.user = data;
|
|
246
270
|
this.isSuperAuthor = !!data.sign;
|
|
247
271
|
this.isTeammate = this.user?.is_teammate;
|
|
@@ -52,6 +52,7 @@ export const Default = {
|
|
|
52
52
|
{ name: 'type / value', type: 'String', default: '""', description: '当前激活项。' },
|
|
53
53
|
{ name: 'clients', type: 'Object', default: 'null', description: 'clientBy 自定义客户端列表。' },
|
|
54
54
|
{ name: 'showWujie', type: 'Boolean', default: 'false', description: 'clientBy 是否展示无界。' },
|
|
55
|
+
{ name: 'autoDetect', type: 'Boolean', default: 'true', description: '是否在挂载时根据站点 hostname 自动选择客户端;受控场景传 false。' },
|
|
55
56
|
],
|
|
56
57
|
},
|
|
57
58
|
{
|
|
@@ -17,6 +17,7 @@ export const Default = {
|
|
|
17
17
|
{ name: 'type', type: 'String', default: '""', description: '当前选中的客户端值。' },
|
|
18
18
|
{ name: 'clients', type: 'Object', default: 'null', description: '自定义客户端映射。' },
|
|
19
19
|
{ name: 'showWujie', type: 'Boolean', default: 'false', description: '是否显示无界。' },
|
|
20
|
+
{ name: 'autoDetect', type: 'Boolean', default: 'true', description: '是否根据站点 hostname 自动选择客户端;受控场景传 false。' },
|
|
20
21
|
] };
|
|
21
22
|
},
|
|
22
23
|
template: `<div style="display:grid;gap:24px;"><section style="border-radius:20px;background:rgba(255,255,255,0.92);padding:24px;box-shadow:0 24px 60px rgba(15,23,42,0.08);"><clientBy :show-wujie="true" /></section><StoryPropsTable title="clientBy" description="客户端筛选组件。" :items="propsInfo" /></div>`,
|
|
@@ -11,6 +11,42 @@ function canUseLocalStorage() {
|
|
|
11
11
|
return typeof localStorage !== "undefined";
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function canUseSessionStorage() {
|
|
15
|
+
return typeof sessionStorage !== "undefined";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function syncActiveAuthToken(token = "") {
|
|
19
|
+
if (!token) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (canUseLocalStorage()) {
|
|
24
|
+
localStorage.setItem("__token", token);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// URL token 会被 common 缓存在 sessionStorage,存在时也必须随账号切换一起更新。
|
|
28
|
+
if (canUseSessionStorage() && sessionStorage.getItem("__token")) {
|
|
29
|
+
sessionStorage.setItem("__token", token);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function hasActiveAuthTokenOverride() {
|
|
36
|
+
const localToken = canUseLocalStorage() ? localStorage.getItem("__token") : "";
|
|
37
|
+
const sessionToken = canUseSessionStorage() ? sessionStorage.getItem("__token") : "";
|
|
38
|
+
return !!(localToken || sessionToken);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clearActiveAuthToken() {
|
|
42
|
+
if (canUseLocalStorage()) {
|
|
43
|
+
localStorage.removeItem("__token");
|
|
44
|
+
}
|
|
45
|
+
if (canUseSessionStorage()) {
|
|
46
|
+
sessionStorage.removeItem("__token");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
14
50
|
function decodeBase64Url(value = "") {
|
|
15
51
|
const normalized = String(value).replace(/-/g, "+").replace(/_/g, "/");
|
|
16
52
|
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
|
|
@@ -120,8 +156,8 @@ async function applyRefreshResult(data = {}) {
|
|
|
120
156
|
|
|
121
157
|
await User.update(profile);
|
|
122
158
|
|
|
123
|
-
// @jx3box/common User.update only writes token; keep
|
|
124
|
-
|
|
159
|
+
// @jx3box/common User.update only writes token; keep higher-priority request tokens in sync too.
|
|
160
|
+
syncActiveAuthToken(profile.token);
|
|
125
161
|
updateCurrentAlternate(profile);
|
|
126
162
|
return true;
|
|
127
163
|
}
|
|
@@ -129,7 +165,7 @@ async function applyRefreshResult(data = {}) {
|
|
|
129
165
|
export async function refreshTokenIfNeeded(options = {}) {
|
|
130
166
|
const { force = false } = options;
|
|
131
167
|
|
|
132
|
-
if (!User.isLogin() || !isAutoTokenRefreshEnabled()) {
|
|
168
|
+
if (!User.isLogin() || (!force && !isAutoTokenRefreshEnabled())) {
|
|
133
169
|
return false;
|
|
134
170
|
}
|
|
135
171
|
|
|
@@ -3,6 +3,8 @@ const path = require("path");
|
|
|
3
3
|
|
|
4
4
|
const root = path.resolve(__dirname, "..");
|
|
5
5
|
const commonHeader = fs.readFileSync(path.join(root, "src/CommonHeader.vue"), "utf8");
|
|
6
|
+
const alternate = fs.readFileSync(path.join(root, "src/header/alternate.vue"), "utf8");
|
|
7
|
+
const userInfo = fs.readFileSync(path.join(root, "src/header/userInfo.vue"), "utf8");
|
|
6
8
|
const cmsService = fs.readFileSync(path.join(root, "service/cms.js"), "utf8");
|
|
7
9
|
const refreshUtil = fs.readFileSync(path.join(root, "src/utils/auth-token-refresh.js"), "utf8");
|
|
8
10
|
const docs = fs.readFileSync(path.join(root, "docs/agents/auth-token-refresh.md"), "utf8");
|
|
@@ -32,16 +34,54 @@ assert(
|
|
|
32
34
|
"minimum refresh interval should be 1 day"
|
|
33
35
|
);
|
|
34
36
|
|
|
37
|
+
assert(
|
|
38
|
+
refreshUtil.includes("(!force && !isAutoTokenRefreshEnabled())"),
|
|
39
|
+
"explicit account switching and URL-token normalization should bypass the automatic refresh toggle"
|
|
40
|
+
);
|
|
41
|
+
|
|
35
42
|
assert(
|
|
36
43
|
refreshUtil.includes('localStorage.getItem("__token") || localStorage.getItem("token")'),
|
|
37
44
|
"refresh should read __token before token"
|
|
38
45
|
);
|
|
39
46
|
|
|
40
47
|
assert(
|
|
41
|
-
refreshUtil.includes("
|
|
48
|
+
refreshUtil.includes("syncActiveAuthToken(profile.token)"),
|
|
42
49
|
"refresh should keep __token in sync"
|
|
43
50
|
);
|
|
44
51
|
|
|
52
|
+
assert(
|
|
53
|
+
refreshUtil.includes('sessionStorage.getItem("__token")') &&
|
|
54
|
+
refreshUtil.includes('sessionStorage.setItem("__token", token)'),
|
|
55
|
+
"refresh should update an existing URL token stored in sessionStorage"
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
assert(
|
|
59
|
+
alternate.includes("syncActiveAuthToken(item.token)") &&
|
|
60
|
+
alternate.indexOf("syncActiveAuthToken(item.token)") < alternate.indexOf("refreshTokenIfNeeded({ force: true })"),
|
|
61
|
+
"alternate switching should synchronize the selected token before refreshing auth"
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
assert(
|
|
65
|
+
alternate.includes("clearActiveAuthToken()") &&
|
|
66
|
+
alternate.indexOf("clearActiveAuthToken()") < alternate.indexOf('location.href = JX3BOX.__Links.account.login'),
|
|
67
|
+
"adding an alternate should clear the previous account override token before login"
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
assert(
|
|
71
|
+
userInfo.includes("clearActiveAuthToken()") && commonHeader.includes("clearActiveAuthToken()"),
|
|
72
|
+
"manual and token-version logout should clear higher-priority request tokens"
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
assert(
|
|
76
|
+
userInfo.includes("repairMismatchedIdentity") && userInfo.includes("hasActiveAuthTokenOverride()"),
|
|
77
|
+
"header profile should repair a cached uid that conflicts with the authenticated request uid"
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
assert(
|
|
81
|
+
commonHeader.includes("reloadOnIdentityChange") && commonHeader.includes("refreshTokenIfNeeded({ force })"),
|
|
82
|
+
"URL token entry should normalize the cached identity and reload when the uid changes"
|
|
83
|
+
);
|
|
84
|
+
|
|
45
85
|
assert(
|
|
46
86
|
refreshUtil.includes("jx3box-alternate-${profile.uid}"),
|
|
47
87
|
"refresh should update the current alternate account cache"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const test = require("node:test");
|
|
5
|
+
|
|
6
|
+
function loadComponentOptions() {
|
|
7
|
+
const filename = path.resolve(__dirname, "../src/filters/clientBy.vue");
|
|
8
|
+
const source = fs.readFileSync(filename, "utf8");
|
|
9
|
+
const script = source.match(/<script>([\s\S]*?)<\/script>/)?.[1] || "";
|
|
10
|
+
const executable = script
|
|
11
|
+
.replace(/^\s*import\s+[^;]+;\s*$/gm, "")
|
|
12
|
+
.replace(/export\s+default\s+/, "return ");
|
|
13
|
+
return Function("i18nMixin", executable)({});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createVm({ type = "", autoDetect = false } = {}) {
|
|
17
|
+
const events = [];
|
|
18
|
+
return {
|
|
19
|
+
vm: {
|
|
20
|
+
type,
|
|
21
|
+
autoDetect,
|
|
22
|
+
client: type || "",
|
|
23
|
+
$emit(name, payload) {
|
|
24
|
+
events.push({ name, payload });
|
|
25
|
+
},
|
|
26
|
+
$forceUpdate() {},
|
|
27
|
+
},
|
|
28
|
+
events,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const component = loadComponentOptions();
|
|
33
|
+
|
|
34
|
+
test("clientBy keeps automatic site detection enabled by default", () => {
|
|
35
|
+
assert.equal(component.props.autoDetect.default, true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("controlled mode keeps all selected and does not emit while mounting", () => {
|
|
39
|
+
const { vm, events } = createVm({ type: "all", autoDetect: false });
|
|
40
|
+
|
|
41
|
+
component.mounted.call(vm);
|
|
42
|
+
|
|
43
|
+
assert.equal(vm.client, "all");
|
|
44
|
+
assert.deepEqual(events, []);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("external type updates only synchronize selection", () => {
|
|
48
|
+
const { vm, events } = createVm({ type: "all", autoDetect: false });
|
|
49
|
+
|
|
50
|
+
component.watch.type.call(vm, "origin");
|
|
51
|
+
|
|
52
|
+
assert.equal(vm.client, "origin");
|
|
53
|
+
assert.deepEqual(events, []);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("user selection emits the existing filter payload", () => {
|
|
57
|
+
const { vm, events } = createVm({ type: "all", autoDetect: false });
|
|
58
|
+
|
|
59
|
+
component.methods.filter.call(vm, "std");
|
|
60
|
+
|
|
61
|
+
assert.equal(vm.client, "std");
|
|
62
|
+
assert.deepEqual(events, [{ name: "filter", payload: { type: "client", val: "std" } }]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("automatic mode only detects origin from hostname", () => {
|
|
66
|
+
const previousLocation = global.location;
|
|
67
|
+
global.location = { hostname: "origin.jx3box.com" };
|
|
68
|
+
const { vm, events } = createVm({ autoDetect: true });
|
|
69
|
+
vm.filter = component.methods.filter;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
component.mounted.call(vm);
|
|
73
|
+
} finally {
|
|
74
|
+
global.location = previousLocation;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
assert.equal(vm.client, "origin");
|
|
78
|
+
assert.deepEqual(events, [{ name: "filter", payload: { type: "client", val: "origin" } }]);
|
|
79
|
+
});
|