itube-specs 0.0.777 → 0.0.779
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/components/adv/native-banner.vue +135 -0
- package/components/adv/native-bottom.vue +25 -0
- package/components/adv/native-player.vue +23 -0
- package/components/adv/ntv.vue +19 -0
- package/components/auth/__tests__/login.test.ts +123 -0
- package/components/auth/__tests__/recovery.test.ts +86 -0
- package/components/auth/__tests__/register.test.ts +122 -0
- package/components/auth/icon.vue +41 -0
- package/components/auth/login.vue +183 -0
- package/components/auth/popup.vue +87 -0
- package/components/auth/recovery.vue +147 -0
- package/components/auth/register.vue +245 -0
- package/components/card/category-mini.vue +62 -0
- package/components/card/category.vue +138 -0
- package/components/card/channel-mini.vue +96 -0
- package/components/card/channel.vue +219 -0
- package/components/card/model-mini.vue +101 -0
- package/components/card/model.vue +224 -0
- package/components/card/playlist-mini.vue +124 -0
- package/components/card/playlist.vue +443 -0
- package/components/card/round.vue +127 -0
- package/components/card/video/dropdown.vue +142 -0
- package/components/card/video/index.vue +460 -0
- package/components/card/video/mini.vue +133 -0
- package/components/card/video/preview.vue +27 -0
- package/components/category/by-letter.vue +55 -0
- package/components/category/group.vue +103 -0
- package/components/category/letter-groups.vue +38 -0
- package/components/category/list.vue +147 -0
- package/components/category/nav.vue +45 -0
- package/components/filter/btn.vue +69 -0
- package/components/filter/chips.vue +57 -0
- package/components/filter/main.vue +305 -0
- package/components/filter/model-field.vue +101 -0
- package/components/filter/model.vue +239 -0
- package/components/filter/page.vue +156 -0
- package/components/filter/popup.vue +320 -0
- package/components/filter/selects.vue +346 -0
- package/components/filter/slider.vue +121 -0
- package/components/filter/videos-chips.vue +96 -0
- package/components/grid/categories.vue +36 -0
- package/components/grid/channels.vue +41 -0
- package/components/grid/models.vue +38 -0
- package/components/grid/playlists.vue +36 -0
- package/components/grid/videos.vue +97 -0
- package/components/info/grid.vue +188 -0
- package/components/info/main.vue +462 -0
- package/components/info/socials.vue +74 -0
- package/components/layout/burger-item.vue +132 -0
- package/components/layout/burger.vue +257 -0
- package/components/layout/cookie.vue +52 -0
- package/components/layout/extra-links.vue +56 -0
- package/components/layout/footer-language.vue +123 -0
- package/components/layout/footer.vue +86 -0
- package/components/layout/header-user.vue +265 -0
- package/components/layout/header.vue +231 -0
- package/components/layout/language.vue +371 -0
- package/components/layout/logo.vue +39 -0
- package/components/layout/nav.vue +173 -0
- package/components/layout/skip-link.vue +26 -0
- package/components/player/__tests__/like.test.ts +85 -0
- package/components/player/aside-videos.vue +43 -0
- package/components/player/autoplay.vue +51 -0
- package/components/player/channel.vue +139 -0
- package/components/player/core.vue +276 -0
- package/components/player/follow.vue +90 -0
- package/components/player/info.vue +61 -0
- package/components/player/interface.vue +230 -0
- package/components/player/like.vue +200 -0
- package/components/player/main.vue +538 -0
- package/components/player/playlist.vue +448 -0
- package/components/player/sponsored.vue +95 -0
- package/components/playlist/__tests__/add.test.ts +115 -0
- package/components/playlist/__tests__/edit.test.ts +91 -0
- package/components/playlist/add.vue +409 -0
- package/components/playlist/delete-video.vue +118 -0
- package/components/playlist/edit.vue +304 -0
- package/components/playlist/input.vue +207 -0
- package/components/playlist/main.vue +152 -0
- package/components/playlist/private-toggle.vue +19 -0
- package/components/profile/__tests__/password.test.ts +60 -0
- package/components/profile/__tests__/setup.test.ts +100 -0
- package/components/profile/delete.vue +61 -0
- package/components/profile/guest.vue +65 -0
- package/components/profile/password.vue +108 -0
- package/components/profile/setup.vue +393 -0
- package/components/report/__tests__/main.test.ts +106 -0
- package/components/report/main.vue +427 -0
- package/components/report/wrapper.vue +15 -0
- package/components/search/input.vue +169 -0
- package/components/search/item.vue +125 -0
- package/components/search/main.vue +178 -0
- package/components/search/result.vue +268 -0
- package/components/section/categories-island.server.vue +29 -0
- package/components/section/categories.vue +24 -0
- package/components/section/models.vue +55 -0
- package/components/section/nav-links.vue +82 -0
- package/components/section/seo-text.vue +74 -0
- package/components/section/swiper-cards.vue +91 -0
- package/components/section/toolbar.vue +136 -0
- package/components/share/main.vue +283 -0
- package/components/ui/avatar.vue +51 -0
- package/components/ui/breadcrumbs.vue +97 -0
- package/components/ui/btn.vue +166 -0
- package/components/ui/checkbox.vue +153 -0
- package/components/ui/chips.vue +236 -0
- package/components/ui/count.vue +31 -0
- package/components/ui/dropdown.vue +403 -0
- package/components/ui/empty-state.vue +67 -0
- package/components/ui/expand-row.vue +200 -0
- package/components/ui/full-descr.vue +37 -0
- package/components/ui/icon.vue +24 -0
- package/components/ui/img.vue +57 -0
- package/components/ui/input.vue +354 -0
- package/components/ui/label.vue +95 -0
- package/components/ui/letter.vue +28 -0
- package/components/ui/link.vue +44 -0
- package/components/ui/notification.vue +160 -0
- package/components/ui/pagination.vue +294 -0
- package/components/ui/popup.vue +408 -0
- package/components/ui/radio.vue +142 -0
- package/components/ui/section-title.vue +55 -0
- package/components/ui/segmented.vue +121 -0
- package/components/ui/select.vue +225 -0
- package/components/ui/slider.vue +395 -0
- package/components/ui/snackbar.vue +103 -0
- package/components/ui/swiper.vue +189 -0
- package/components/ui/toggle.vue +112 -0
- package/components/ui/tooltip.vue +109 -0
- package/components/view/model-videos.vue +312 -0
- package/components/view/models.vue +228 -0
- package/components/view/video.vue +263 -0
- package/components/view/videos.vue +202 -0
- package/composables/__tests__/use-next-video.test.ts +123 -0
- package/composables/fetch/use-fetch-video.ts +28 -4
- package/composables/use-next-video.ts +120 -0
- package/package.json +1 -1
- package/runtime/constants/adverts.ts +18 -0
- package/runtime/index.ts +1 -0
- package/types/index.d.ts +1 -0
- package/types/site.d.ts +37 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ClientOnly>
|
|
3
|
+
<div v-if="spot"
|
|
4
|
+
:class="{
|
|
5
|
+
'ntv': isNtv,
|
|
6
|
+
}"
|
|
7
|
+
class="adv-native-card"
|
|
8
|
+
>
|
|
9
|
+
<div v-once
|
|
10
|
+
class="adv-native-card__slot"
|
|
11
|
+
>
|
|
12
|
+
<div data-asg-ins
|
|
13
|
+
:data-spots="spot"
|
|
14
|
+
/>
|
|
15
|
+
</div>
|
|
16
|
+
<div v-if="!isNtv"
|
|
17
|
+
class="adv-native-card__bottom"
|
|
18
|
+
>
|
|
19
|
+
<UiImg :alt="t('advertising_banner')"
|
|
20
|
+
class="adv-native-card__channel-logo"
|
|
21
|
+
width="32"
|
|
22
|
+
height="32"
|
|
23
|
+
src="/img/user-uploaded.svg"
|
|
24
|
+
sizes="32px"
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
<div class="adv-native-card__titles">
|
|
28
|
+
<p class="adv-native-card__secondary">
|
|
29
|
+
{{ t('advertising_banner') }}
|
|
30
|
+
</p>
|
|
31
|
+
<p class="adv-native-card__primary">
|
|
32
|
+
{{ title }}
|
|
33
|
+
</p>
|
|
34
|
+
<p class="adv-native-card__secondary">
|
|
35
|
+
{{ text }}
|
|
36
|
+
</p>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
</ClientOnly>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup lang="ts">
|
|
44
|
+
defineProps<{
|
|
45
|
+
isNtv: boolean;
|
|
46
|
+
spot: number,
|
|
47
|
+
}>();
|
|
48
|
+
|
|
49
|
+
const { t } = useI18n();
|
|
50
|
+
|
|
51
|
+
const title = ref(t('loading'));
|
|
52
|
+
const text = ref(t('loading'));
|
|
53
|
+
|
|
54
|
+
const freeze = ref(false);
|
|
55
|
+
|
|
56
|
+
function getBannerTexts() {
|
|
57
|
+
let counter = 0;
|
|
58
|
+
const limit = 50;
|
|
59
|
+
|
|
60
|
+
function getBannerTextInner() {
|
|
61
|
+
const titleContainer = document?.querySelector('.exo-native-widget-item-title');
|
|
62
|
+
const textContainer = document?.querySelector('.exo-native-widget-item-text');
|
|
63
|
+
|
|
64
|
+
if (counter < limit && !titleContainer && !textContainer) {
|
|
65
|
+
counter++;
|
|
66
|
+
|
|
67
|
+
setTimeout(getBannerTextInner, 50);
|
|
68
|
+
} else {
|
|
69
|
+
title.value = titleContainer?.innerHTML || '';
|
|
70
|
+
text.value = textContainer?.innerHTML || '';
|
|
71
|
+
|
|
72
|
+
freeze.value = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getBannerTextInner();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
onMounted(() => {
|
|
80
|
+
getBannerTexts();
|
|
81
|
+
});
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<style lang="scss">
|
|
85
|
+
.adv-native-card {
|
|
86
|
+
@include video-card;
|
|
87
|
+
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
margin-bottom: 300px;
|
|
90
|
+
background-color: $native-card-bg;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.adv-native-card__bottom {
|
|
94
|
+
display: grid;
|
|
95
|
+
align-items: start;
|
|
96
|
+
align-content: start;
|
|
97
|
+
grid-template-columns: 32px 1fr;
|
|
98
|
+
gap: 8px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.adv-native-card__titles {
|
|
102
|
+
display: flex;
|
|
103
|
+
flex-direction: column;
|
|
104
|
+
gap: 4px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.adv-native-card__primary {
|
|
108
|
+
color: $native-card-primary;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.adv-native-card__secondary {
|
|
112
|
+
font-size: 12px;
|
|
113
|
+
color: $native-card-secondary;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.adv-native-card__slot {
|
|
117
|
+
@include video-card-poster;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ntv .adv-native-card__slot {
|
|
121
|
+
aspect-ratio: 1 / 1;
|
|
122
|
+
width: 300px;
|
|
123
|
+
height: 250px;
|
|
124
|
+
align-self: center;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.adv-native-card__slot > div {
|
|
128
|
+
position: absolute;
|
|
129
|
+
inset: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.adv-native-card__slot div {
|
|
133
|
+
width: 100% !important;
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<iframe :src="`/embed/${BannerHelper.getBannerSpot(AdBannerType.NativeBottom, AdSpotType.Primary)}`"
|
|
3
|
+
class="adv-native-bottom"
|
|
4
|
+
title="content"
|
|
5
|
+
/>
|
|
6
|
+
<iframe :src="`/embed/${BannerHelper.getBannerSpot(AdBannerType.NativeBottom, AdSpotType.Secondary)}`"
|
|
7
|
+
class="adv-native-bottom"
|
|
8
|
+
title="content"
|
|
9
|
+
/>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { BannerHelper } from '~/utils/banner-helper';
|
|
14
|
+
import { AdBannerType, AdSpotType } from '../../runtime';
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<style lang="scss">
|
|
18
|
+
.adv-native-bottom {
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: auto;
|
|
21
|
+
min-height: 240px;
|
|
22
|
+
aspect-ratio: 285 / 248;
|
|
23
|
+
border: none;
|
|
24
|
+
}
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<iframe :src="`/embed/${BannerHelper.getBannerSpot(AdBannerType.NativePlayer, AdSpotType.Primary)}`"
|
|
3
|
+
class="adv-native-player"
|
|
4
|
+
title="content"
|
|
5
|
+
/>
|
|
6
|
+
<iframe :src="`/embed/${BannerHelper.getBannerSpot(AdBannerType.NativePlayer, AdSpotType.Secondary)}`"
|
|
7
|
+
class="adv-native-player"
|
|
8
|
+
title="content"
|
|
9
|
+
/>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
import { BannerHelper } from '~/utils/banner-helper';
|
|
14
|
+
import { AdBannerType, AdSpotType } from '../../runtime';
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<style lang="scss">
|
|
18
|
+
.adv-native-player {
|
|
19
|
+
aspect-ratio: 7 / 5.7;
|
|
20
|
+
border: none;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<iframe :src="`/embed/${BannerHelper.getBannerSpot(AdBannerType.NTV, AdSpotType.Primary)}?ntv=1`"
|
|
3
|
+
class="adv-ntv"
|
|
4
|
+
title="content"
|
|
5
|
+
/>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
|
|
10
|
+
import { BannerHelper } from '~/utils/banner-helper';
|
|
11
|
+
import { AdBannerType, AdSpotType } from '../../runtime';
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<style lang="scss">
|
|
15
|
+
.adv-ntv {
|
|
16
|
+
aspect-ratio: 1 / 1;
|
|
17
|
+
border: none;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// @vitest-environment nuxt
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime';
|
|
4
|
+
import { flushPromises } from '@vue/test-utils';
|
|
5
|
+
import AuthLogin from '../login.vue';
|
|
6
|
+
|
|
7
|
+
const h = vi.hoisted(() => {
|
|
8
|
+
const login = vi.fn();
|
|
9
|
+
const closeAuthPopup = vi.fn();
|
|
10
|
+
const showError = vi.fn();
|
|
11
|
+
const showSuccess = vi.fn();
|
|
12
|
+
const setErrorState = vi.fn();
|
|
13
|
+
const resetSnackbar = vi.fn();
|
|
14
|
+
return {
|
|
15
|
+
login,
|
|
16
|
+
closeAuthPopup,
|
|
17
|
+
showError,
|
|
18
|
+
showSuccess,
|
|
19
|
+
setErrorState,
|
|
20
|
+
resetSnackbar,
|
|
21
|
+
useUserMock: vi.fn(() => ({ login })),
|
|
22
|
+
useAuthPopupMock: vi.fn(() => ({ closeAuthPopup })),
|
|
23
|
+
useSnackbarMock: vi.fn(() => ({ showError, showSuccess, setErrorState, resetSnackbar })),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
mockNuxtImport('useUser', () => h.useUserMock);
|
|
28
|
+
mockNuxtImport('useAuthPopup', () => h.useAuthPopupMock);
|
|
29
|
+
mockNuxtImport('useSnackbar', () => h.useSnackbarMock);
|
|
30
|
+
|
|
31
|
+
const VALID_USER = 'john_doe';
|
|
32
|
+
const VALID_PASS = 'secret';
|
|
33
|
+
|
|
34
|
+
function mountLogin() {
|
|
35
|
+
return mountSuspended(AuthLogin, {
|
|
36
|
+
global: { stubs: { AuthIcon: true, UiIcon: true } },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function setCredentials(wrapper: Awaited<ReturnType<typeof mountLogin>>, user: string, pass: string) {
|
|
41
|
+
const inputs = wrapper.findAll('input');
|
|
42
|
+
await inputs[0].setValue(user);
|
|
43
|
+
await inputs[1].setValue(pass);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.clearAllMocks();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('AuthLogin', () => {
|
|
51
|
+
it('эмитит forgot по клику «забыли пароль»', async () => {
|
|
52
|
+
const wrapper = await mountLogin();
|
|
53
|
+
await wrapper.find('.auth-login__forgot').trigger('click');
|
|
54
|
+
expect(wrapper.emitted('forgot')).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('эмитит create по клику «регистрация»', async () => {
|
|
58
|
+
const wrapper = await mountLogin();
|
|
59
|
+
await wrapper.find('.auth-login__back-button').trigger('click');
|
|
60
|
+
expect(wrapper.emitted('create')).toBeTruthy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('невалидный username → поле в ошибке, снекбар error_username, login не вызывается', async () => {
|
|
64
|
+
const wrapper = await mountLogin();
|
|
65
|
+
await setCredentials(wrapper, 'a', VALID_PASS);
|
|
66
|
+
await wrapper.find('.auth-login__login').trigger('click');
|
|
67
|
+
await flushPromises();
|
|
68
|
+
expect(wrapper.findAll('.ui-input')[0].classes()).toContain('ui-input--error');
|
|
69
|
+
expect(h.showError).toHaveBeenCalledWith('error_username');
|
|
70
|
+
expect(h.login).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('невалидный password → поле в ошибке, снекбар error_password, login не вызывается', async () => {
|
|
74
|
+
const wrapper = await mountLogin();
|
|
75
|
+
await setCredentials(wrapper, VALID_USER, 'abc');
|
|
76
|
+
await wrapper.find('.auth-login__login').trigger('click');
|
|
77
|
+
await flushPromises();
|
|
78
|
+
expect(wrapper.findAll('.ui-input')[1].classes()).toContain('ui-input--error');
|
|
79
|
+
expect(h.showError).toHaveBeenCalledWith('error_password');
|
|
80
|
+
expect(h.login).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('login очищает снекбар в начале (resetSnackbar)', async () => {
|
|
84
|
+
const wrapper = await mountLogin();
|
|
85
|
+
await setCredentials(wrapper, 'a', 'abc');
|
|
86
|
+
await wrapper.find('.auth-login__login').trigger('click');
|
|
87
|
+
await flushPromises();
|
|
88
|
+
expect(h.resetSnackbar).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('валидные данные → login(form), снекбар успеха, закрытие попапа', async () => {
|
|
92
|
+
h.login.mockResolvedValueOnce(undefined);
|
|
93
|
+
const wrapper = await mountLogin();
|
|
94
|
+
await setCredentials(wrapper, VALID_USER, VALID_PASS);
|
|
95
|
+
await wrapper.find('.auth-login__login').trigger('click');
|
|
96
|
+
await flushPromises();
|
|
97
|
+
expect(h.login).toHaveBeenCalledWith({ username: VALID_USER, password_hash: VALID_PASS });
|
|
98
|
+
expect(h.closeAuthPopup).toHaveBeenCalled();
|
|
99
|
+
expect(h.showSuccess).toHaveBeenCalledWith('Welcome back!');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('ошибка login → снекбар ошибки (setErrorState), попап не закрывается', async () => {
|
|
103
|
+
const err = new Error('bad creds');
|
|
104
|
+
h.login.mockRejectedValueOnce(err);
|
|
105
|
+
const wrapper = await mountLogin();
|
|
106
|
+
await setCredentials(wrapper, VALID_USER, VALID_PASS);
|
|
107
|
+
await wrapper.find('.auth-login__login').trigger('click');
|
|
108
|
+
await flushPromises();
|
|
109
|
+
expect(h.login).toHaveBeenCalled();
|
|
110
|
+
expect(h.setErrorState).toHaveBeenCalledWith(err);
|
|
111
|
+
expect(h.closeAuthPopup).not.toHaveBeenCalled();
|
|
112
|
+
expect(h.showSuccess).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('Enter в поле пароля запускает вход', async () => {
|
|
116
|
+
h.login.mockResolvedValueOnce(undefined);
|
|
117
|
+
const wrapper = await mountLogin();
|
|
118
|
+
await setCredentials(wrapper, VALID_USER, VALID_PASS);
|
|
119
|
+
await wrapper.find('input[type="password"]').trigger('keyup.enter');
|
|
120
|
+
await flushPromises();
|
|
121
|
+
expect(h.login).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// @vitest-environment nuxt
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime';
|
|
4
|
+
import { flushPromises } from '@vue/test-utils';
|
|
5
|
+
import AuthRecovery from '../recovery.vue';
|
|
6
|
+
|
|
7
|
+
const h = vi.hoisted(() => {
|
|
8
|
+
const password = vi.fn();
|
|
9
|
+
const closeAuthPopup = vi.fn();
|
|
10
|
+
const showError = vi.fn();
|
|
11
|
+
const showSuccess = vi.fn();
|
|
12
|
+
const setErrorState = vi.fn();
|
|
13
|
+
const resetSnackbar = vi.fn();
|
|
14
|
+
const getRecaptchaToken = vi.fn(() => Promise.resolve('test-token'));
|
|
15
|
+
const initRecaptcha = vi.fn(() => Promise.resolve());
|
|
16
|
+
return {
|
|
17
|
+
password,
|
|
18
|
+
closeAuthPopup,
|
|
19
|
+
showError,
|
|
20
|
+
showSuccess,
|
|
21
|
+
setErrorState,
|
|
22
|
+
resetSnackbar,
|
|
23
|
+
getRecaptchaToken,
|
|
24
|
+
initRecaptcha,
|
|
25
|
+
useUserMock: vi.fn(() => ({ password })),
|
|
26
|
+
useAuthPopupMock: vi.fn(() => ({ closeAuthPopup })),
|
|
27
|
+
useSnackbarMock: vi.fn(() => ({ showError, showSuccess, setErrorState, resetSnackbar })),
|
|
28
|
+
useRecaptchaMock: vi.fn(() => ({ getRecaptchaToken, initRecaptcha })),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
mockNuxtImport('useUser', () => h.useUserMock);
|
|
33
|
+
mockNuxtImport('useAuthPopup', () => h.useAuthPopupMock);
|
|
34
|
+
mockNuxtImport('useSnackbar', () => h.useSnackbarMock);
|
|
35
|
+
mockNuxtImport('useRecaptcha', () => h.useRecaptchaMock);
|
|
36
|
+
|
|
37
|
+
function mountRecovery() {
|
|
38
|
+
return mountSuspended(AuthRecovery, {
|
|
39
|
+
global: { stubs: { AuthIcon: true, UiIcon: true } },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('AuthRecovery', () => {
|
|
48
|
+
it('эмитит login по клику «назад ко входу»', async () => {
|
|
49
|
+
const wrapper = await mountRecovery();
|
|
50
|
+
await wrapper.find('.auth-recovery__login').trigger('click');
|
|
51
|
+
expect(wrapper.emitted('login')).toBeTruthy();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('невалидный email → поле в ошибке, снекбар error_email, password не вызывается', async () => {
|
|
55
|
+
const wrapper = await mountRecovery();
|
|
56
|
+
await wrapper.find('input').setValue('bad');
|
|
57
|
+
await wrapper.find('.auth-recovery__button').trigger('click');
|
|
58
|
+
await flushPromises();
|
|
59
|
+
expect(wrapper.find('.ui-input').classes()).toContain('ui-input--error');
|
|
60
|
+
expect(h.showError).toHaveBeenCalledWith('error_email');
|
|
61
|
+
expect(h.password).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('валидный email → password(form с токеном), закрытие попапа, снекбар успеха', async () => {
|
|
65
|
+
h.password.mockResolvedValueOnce(undefined);
|
|
66
|
+
const wrapper = await mountRecovery();
|
|
67
|
+
await wrapper.find('input').setValue('user@example.com');
|
|
68
|
+
await wrapper.find('.auth-recovery__button').trigger('click');
|
|
69
|
+
await flushPromises();
|
|
70
|
+
expect(h.getRecaptchaToken).toHaveBeenCalledWith('password_reset');
|
|
71
|
+
expect(h.password).toHaveBeenCalledWith({ email: 'user@example.com', token: 'test-token' });
|
|
72
|
+
expect(h.closeAuthPopup).toHaveBeenCalled();
|
|
73
|
+
expect(h.showSuccess).toHaveBeenCalledWith('Check your email');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('ошибка запроса → снекбар ошибки, попап не закрывается', async () => {
|
|
77
|
+
const err = new Error('not found');
|
|
78
|
+
h.password.mockRejectedValueOnce(err);
|
|
79
|
+
const wrapper = await mountRecovery();
|
|
80
|
+
await wrapper.find('input').setValue('user@example.com');
|
|
81
|
+
await wrapper.find('.auth-recovery__button').trigger('click');
|
|
82
|
+
await flushPromises();
|
|
83
|
+
expect(h.setErrorState).toHaveBeenCalledWith(err);
|
|
84
|
+
expect(h.closeAuthPopup).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @vitest-environment nuxt
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime';
|
|
4
|
+
import { flushPromises } from '@vue/test-utils';
|
|
5
|
+
import AuthRegister from '../register.vue';
|
|
6
|
+
|
|
7
|
+
const h = vi.hoisted(() => {
|
|
8
|
+
const register = vi.fn();
|
|
9
|
+
const closeAuthPopup = vi.fn();
|
|
10
|
+
const showError = vi.fn();
|
|
11
|
+
const showSuccess = vi.fn();
|
|
12
|
+
const setErrorState = vi.fn();
|
|
13
|
+
const resetSnackbar = vi.fn();
|
|
14
|
+
const getRecaptchaToken = vi.fn(() => Promise.resolve('test-token'));
|
|
15
|
+
const initRecaptcha = vi.fn(() => Promise.resolve());
|
|
16
|
+
return {
|
|
17
|
+
register,
|
|
18
|
+
closeAuthPopup,
|
|
19
|
+
showError,
|
|
20
|
+
showSuccess,
|
|
21
|
+
setErrorState,
|
|
22
|
+
resetSnackbar,
|
|
23
|
+
getRecaptchaToken,
|
|
24
|
+
initRecaptcha,
|
|
25
|
+
useUserMock: vi.fn(() => ({ register })),
|
|
26
|
+
useAuthPopupMock: vi.fn(() => ({ closeAuthPopup })),
|
|
27
|
+
useSnackbarMock: vi.fn(() => ({ showError, showSuccess, setErrorState, resetSnackbar })),
|
|
28
|
+
useRecaptchaMock: vi.fn(() => ({ getRecaptchaToken, initRecaptcha })),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
mockNuxtImport('useUser', () => h.useUserMock);
|
|
33
|
+
mockNuxtImport('useAuthPopup', () => h.useAuthPopupMock);
|
|
34
|
+
mockNuxtImport('useSnackbar', () => h.useSnackbarMock);
|
|
35
|
+
mockNuxtImport('useRecaptcha', () => h.useRecaptchaMock);
|
|
36
|
+
|
|
37
|
+
const VALID = { email: 'user@example.com', username: 'john_doe', password: 'secret' };
|
|
38
|
+
|
|
39
|
+
function mountRegister() {
|
|
40
|
+
return mountSuspended(AuthRegister, {
|
|
41
|
+
global: { stubs: { AuthIcon: true, UiIcon: true } },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fillForm(
|
|
46
|
+
wrapper: Awaited<ReturnType<typeof mountRegister>>,
|
|
47
|
+
email: string,
|
|
48
|
+
username: string,
|
|
49
|
+
password: string,
|
|
50
|
+
) {
|
|
51
|
+
await wrapper.find('input[type="email"]').setValue(email);
|
|
52
|
+
await wrapper.find('input[name="login-name"]').setValue(username);
|
|
53
|
+
await wrapper.find('input[type="password"]').setValue(password);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('AuthRegister', () => {
|
|
61
|
+
it('эмитит login по клику «уже есть аккаунт»', async () => {
|
|
62
|
+
const wrapper = await mountRegister();
|
|
63
|
+
await wrapper.find('.auth-register__login-button').trigger('click');
|
|
64
|
+
expect(wrapper.emitted('login')).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('невалидный email → снекбар error_email, register не вызывается', async () => {
|
|
68
|
+
const wrapper = await mountRegister();
|
|
69
|
+
await fillForm(wrapper, 'bad', VALID.username, VALID.password);
|
|
70
|
+
await wrapper.find('.ui-btn').trigger('click');
|
|
71
|
+
await flushPromises();
|
|
72
|
+
expect(h.showError).toHaveBeenCalledWith('error_email');
|
|
73
|
+
expect(h.register).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('невалидный username → снекбар error_username, register не вызывается', async () => {
|
|
77
|
+
const wrapper = await mountRegister();
|
|
78
|
+
await fillForm(wrapper, VALID.email, 'a', VALID.password);
|
|
79
|
+
await wrapper.find('.ui-btn').trigger('click');
|
|
80
|
+
await flushPromises();
|
|
81
|
+
expect(h.showError).toHaveBeenCalledWith('error_username');
|
|
82
|
+
expect(h.register).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('невалидный password → снекбар error_password, register не вызывается', async () => {
|
|
86
|
+
const wrapper = await mountRegister();
|
|
87
|
+
await fillForm(wrapper, VALID.email, VALID.username, 'abc');
|
|
88
|
+
await wrapper.find('.ui-btn').trigger('click');
|
|
89
|
+
await flushPromises();
|
|
90
|
+
expect(h.showError).toHaveBeenCalledWith('error_password');
|
|
91
|
+
expect(h.register).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('валидные данные → register(form с reCAPTCHA-токеном), закрытие попапа, снекбар успеха', async () => {
|
|
95
|
+
h.register.mockResolvedValueOnce(undefined);
|
|
96
|
+
const wrapper = await mountRegister();
|
|
97
|
+
await fillForm(wrapper, VALID.email, VALID.username, VALID.password);
|
|
98
|
+
await wrapper.find('.ui-btn').trigger('click');
|
|
99
|
+
await flushPromises();
|
|
100
|
+
expect(h.getRecaptchaToken).toHaveBeenCalledWith('register');
|
|
101
|
+
expect(h.register).toHaveBeenCalledWith({
|
|
102
|
+
email: VALID.email,
|
|
103
|
+
username: VALID.username,
|
|
104
|
+
password_hash: VALID.password,
|
|
105
|
+
token: 'test-token',
|
|
106
|
+
});
|
|
107
|
+
expect(h.closeAuthPopup).toHaveBeenCalled();
|
|
108
|
+
expect(h.showSuccess).toHaveBeenCalledWith('Registration success');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('ошибка register → снекбар ошибки, попап не закрывается', async () => {
|
|
112
|
+
const err = new Error('taken');
|
|
113
|
+
h.register.mockRejectedValueOnce(err);
|
|
114
|
+
const wrapper = await mountRegister();
|
|
115
|
+
await fillForm(wrapper, VALID.email, VALID.username, VALID.password);
|
|
116
|
+
await wrapper.find('.ui-btn').trigger('click');
|
|
117
|
+
await flushPromises();
|
|
118
|
+
expect(h.setErrorState).toHaveBeenCalledWith(err);
|
|
119
|
+
expect(h.closeAuthPopup).not.toHaveBeenCalled();
|
|
120
|
+
expect(h.showSuccess).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="auth-icon">
|
|
3
|
+
<UiIcon :name="icon"
|
|
4
|
+
class="auth-icon__icon"
|
|
5
|
+
size="24"
|
|
6
|
+
/>
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
withDefaults(defineProps<{
|
|
12
|
+
icon?: string
|
|
13
|
+
}>(), {
|
|
14
|
+
icon: 'user-filled'
|
|
15
|
+
});
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<style lang="scss">
|
|
19
|
+
.auth-icon {
|
|
20
|
+
--size: 64px;
|
|
21
|
+
|
|
22
|
+
display: grid;
|
|
23
|
+
place-content: center;
|
|
24
|
+
border-radius: $radius-full;
|
|
25
|
+
width: var(--size);
|
|
26
|
+
height: var(--size);
|
|
27
|
+
background-color: $popup-auth-icon-bg;
|
|
28
|
+
|
|
29
|
+
@include from-br(sm) {
|
|
30
|
+
--size: 48px;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.auth-icon__icon {
|
|
35
|
+
--size: 32px;
|
|
36
|
+
|
|
37
|
+
width: var(--size);
|
|
38
|
+
height: var(--size);
|
|
39
|
+
color: $popup-auth-icon;
|
|
40
|
+
}
|
|
41
|
+
</style>
|