nuxt-telegram-mini-app 0.0.1
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/.env.example +2 -0
- package/.vscode/settings.json +55 -0
- package/.vscode/tailwind.json +55 -0
- package/CONTRIBUTING.md +406 -0
- package/LICENSE +21 -0
- package/README.md +640 -0
- package/app/app.vue +87 -0
- package/app/assets/css/main.css +53 -0
- package/app/assets/css/tailwind.css +3 -0
- package/app/components/ErrorBoundary.vue +81 -0
- package/app/components/Hero.vue +61 -0
- package/app/components/tg/Button.vue +128 -0
- package/app/components/tg/Cell.vue +91 -0
- package/app/components/tg/Content.vue +42 -0
- package/app/components/tg/Nav.vue +107 -0
- package/app/components/tg/Section.vue +50 -0
- package/app/composables/telegram.ts +342 -0
- package/app/error.vue +161 -0
- package/app/pages/components.vue +279 -0
- package/app/pages/functions.vue +107 -0
- package/app/pages/index.vue +211 -0
- package/app/pages/utilities.vue +402 -0
- package/app/types/telegram-webapp.ts +160 -0
- package/app/utils/color.ts +37 -0
- package/eslint.config.mjs +6 -0
- package/nuxt.config.ts +55 -0
- package/package.json +46 -0
- package/public/_redirects +2 -0
- package/public/favicon.ico +0 -0
- package/public/img/hero-user.svg +8 -0
- package/public/img/nuxt-logo.svg +11 -0
- package/public/robots.txt +2 -0
- package/server/api/verify-telegram-data.post.ts +150 -0
- package/tailwind.config.ts +39 -0
- package/tests/components.spec.ts +311 -0
- package/tests/pages.spec.ts +426 -0
- package/tests/telegram.spec.ts +105 -0
- package/tests/utils.spec.ts +47 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
3
|
+
import { mount } from '@vue/test-utils'
|
|
4
|
+
import { defineComponent, h, ref, computed } from 'vue'
|
|
5
|
+
|
|
6
|
+
// Mock global process
|
|
7
|
+
Object.defineProperty(globalThis, 'process', {
|
|
8
|
+
value: { client: true }
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
// Mock Nuxt composables
|
|
12
|
+
vi.mock('nuxt/app', () => ({
|
|
13
|
+
useRequestURL: () => ({
|
|
14
|
+
href: 'https://test.example.com'
|
|
15
|
+
})
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
vi.mock('#app', () => ({
|
|
19
|
+
useRequestURL: () => ({
|
|
20
|
+
href: 'https://test.example.com'
|
|
21
|
+
})
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
vi.mock('vue-router', () => {
|
|
25
|
+
const actual = vi.importActual('vue-router')
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
useRouter: () => ({
|
|
29
|
+
push: vi.fn(),
|
|
30
|
+
back: vi.fn(),
|
|
31
|
+
replace: vi.fn(),
|
|
32
|
+
currentRoute: {
|
|
33
|
+
value: { path: '/' }
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Mock Telegram composables
|
|
40
|
+
vi.mock('~/composables/telegram', () => ({
|
|
41
|
+
useHapticFeedback: () => ({
|
|
42
|
+
supported: { value: true },
|
|
43
|
+
impactOccurred: vi.fn(),
|
|
44
|
+
notificationOccurred: vi.fn(),
|
|
45
|
+
selectionChanged: vi.fn()
|
|
46
|
+
}),
|
|
47
|
+
useMainButton: () => ({
|
|
48
|
+
mounted: { value: true },
|
|
49
|
+
visible: { value: false },
|
|
50
|
+
text: { value: 'Test Button' },
|
|
51
|
+
mount: vi.fn(),
|
|
52
|
+
setParams: vi.fn(),
|
|
53
|
+
onClick: vi.fn(() => vi.fn()),
|
|
54
|
+
offClick: vi.fn()
|
|
55
|
+
}),
|
|
56
|
+
useBackButton: () => ({
|
|
57
|
+
mounted: { value: true },
|
|
58
|
+
visible: { value: false },
|
|
59
|
+
mount: vi.fn(),
|
|
60
|
+
show: vi.fn(),
|
|
61
|
+
hide: vi.fn(),
|
|
62
|
+
onClick: vi.fn(() => vi.fn()),
|
|
63
|
+
offClick: vi.fn()
|
|
64
|
+
}),
|
|
65
|
+
useInitData: () => ({
|
|
66
|
+
user: { value: { username: 'testuser', first_name: 'Test', last_name: 'User', id: 123 } },
|
|
67
|
+
queryId: { value: 'test123' },
|
|
68
|
+
startParam: { value: 'start123' },
|
|
69
|
+
state: { value: { user: { username: 'testuser' } } },
|
|
70
|
+
raw: { value: 'test_data=123&user=testuser' },
|
|
71
|
+
restore: vi.fn()
|
|
72
|
+
}),
|
|
73
|
+
useThemeParams: () => ({
|
|
74
|
+
backgroundColor: { value: '#ffffff' },
|
|
75
|
+
textColor: { value: '#000000' },
|
|
76
|
+
buttonColor: { value: '#0088cc' },
|
|
77
|
+
buttonTextColor: { value: '#ffffff' },
|
|
78
|
+
linkColor: { value: '#0088cc' },
|
|
79
|
+
secondaryBackgroundColor: { value: '#f0f0f0' },
|
|
80
|
+
hintColor: { value: '#999999' }
|
|
81
|
+
}),
|
|
82
|
+
useMiniApp: () => ({
|
|
83
|
+
supported: { value: true },
|
|
84
|
+
dark: { value: false },
|
|
85
|
+
setBackgroundColor: vi.fn(),
|
|
86
|
+
setHeaderColor: vi.fn()
|
|
87
|
+
}),
|
|
88
|
+
useViewport: () => ({
|
|
89
|
+
width: { value: 390 },
|
|
90
|
+
height: { value: 844 },
|
|
91
|
+
stableHeight: { value: 844 },
|
|
92
|
+
expanded: { value: true },
|
|
93
|
+
expand: vi.fn(),
|
|
94
|
+
requestFullscreen: vi.fn(),
|
|
95
|
+
exitFullscreen: vi.fn()
|
|
96
|
+
}),
|
|
97
|
+
useTelegramWebApp: () => ({
|
|
98
|
+
webApp: { value: {} },
|
|
99
|
+
isReady: { value: true },
|
|
100
|
+
isAvailable: { value: true }
|
|
101
|
+
}),
|
|
102
|
+
openLink: vi.fn(),
|
|
103
|
+
openTelegramLink: vi.fn(),
|
|
104
|
+
shareURL: vi.fn()
|
|
105
|
+
}))
|
|
106
|
+
|
|
107
|
+
vi.mock('~/utils/color', () => ({
|
|
108
|
+
toHex: (color: string) => {
|
|
109
|
+
if (!color) return '—'
|
|
110
|
+
if (color.startsWith('#')) return color
|
|
111
|
+
if (color === 'bg_color') return '#ffffff'
|
|
112
|
+
return '#000000'
|
|
113
|
+
}
|
|
114
|
+
}))
|
|
115
|
+
|
|
116
|
+
// Create mock components
|
|
117
|
+
const MockTgContent = defineComponent({
|
|
118
|
+
name: 'TgContent',
|
|
119
|
+
setup(_, { slots }) {
|
|
120
|
+
return () => h('main', { class: 'tg-content' }, slots.default?.())
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const MockHero = defineComponent({
|
|
125
|
+
name: 'Hero',
|
|
126
|
+
props: ['title', 'subtitle', 'imageSrc'],
|
|
127
|
+
setup(props) {
|
|
128
|
+
return () => h('header', { class: 'hero' }, [
|
|
129
|
+
h('h1', props.title),
|
|
130
|
+
props.subtitle && h('p', props.subtitle)
|
|
131
|
+
])
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const MockTgSection = defineComponent({
|
|
136
|
+
name: 'TgSection',
|
|
137
|
+
props: ['title', 'inset'],
|
|
138
|
+
setup(props, { slots }) {
|
|
139
|
+
return () => h('section', { class: 'tg-section' }, [
|
|
140
|
+
props.title && h('h2', props.title),
|
|
141
|
+
slots.default?.()
|
|
142
|
+
])
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const MockTgCell = defineComponent({
|
|
147
|
+
name: 'TgCell',
|
|
148
|
+
props: ['title', 'subtitle', 'description', 'icon', 'to', 'border'],
|
|
149
|
+
emits: ['click'],
|
|
150
|
+
setup(props, { emit }) {
|
|
151
|
+
return () => h('div', {
|
|
152
|
+
class: 'tg-cell',
|
|
153
|
+
onClick: () => emit('click')
|
|
154
|
+
}, [
|
|
155
|
+
h('span', props.title),
|
|
156
|
+
props.subtitle && h('small', props.subtitle),
|
|
157
|
+
props.description && h('p', props.description)
|
|
158
|
+
])
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const MockTgButton = defineComponent({
|
|
163
|
+
name: 'TgButton',
|
|
164
|
+
props: ['title', 'status', 'haptic', 'disabled', 'loading', 'shareUrl'],
|
|
165
|
+
emits: ['click'],
|
|
166
|
+
setup(props, { emit }) {
|
|
167
|
+
return () => h('button', {
|
|
168
|
+
onClick: () => emit('click'),
|
|
169
|
+
disabled: props.disabled || props.loading,
|
|
170
|
+
class: 'tg-button'
|
|
171
|
+
}, props.title)
|
|
172
|
+
}
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const MockTgNav = defineComponent({
|
|
176
|
+
name: 'TgNav',
|
|
177
|
+
props: ['modelValue', 'items'],
|
|
178
|
+
emits: ['select', 'update:modelValue'],
|
|
179
|
+
setup(props, { emit }) {
|
|
180
|
+
return () => h('nav', { class: 'tg-nav' },
|
|
181
|
+
props.items?.map((item: any) =>
|
|
182
|
+
h('button', {
|
|
183
|
+
onClick: () => emit('select', item),
|
|
184
|
+
class: item.key === props.modelValue ? 'active' : ''
|
|
185
|
+
}, item.label)
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const MockClientOnly = defineComponent({
|
|
192
|
+
name: 'ClientOnly',
|
|
193
|
+
setup(_, { slots }) {
|
|
194
|
+
return () => slots.default?.()
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Mock router
|
|
199
|
+
const router = createRouter({
|
|
200
|
+
history: createWebHistory(),
|
|
201
|
+
routes: [
|
|
202
|
+
{ path: '/', component: { template: '<div>Home</div>' } },
|
|
203
|
+
{ path: '/components', component: { template: '<div>Components</div>' } },
|
|
204
|
+
{ path: '/utilities', component: { template: '<div>Utils</div>' } },
|
|
205
|
+
{ path: '/functions', component: { template: '<div>Functions</div>' } }
|
|
206
|
+
]
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('Page Components', () => {
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
vi.clearAllMocks()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Navigation Integration', () => {
|
|
215
|
+
it('should render navigation with correct items', () => {
|
|
216
|
+
const navItems = [
|
|
217
|
+
{ key: 'home', label: 'Home', icon: 'i-heroicons-home-20-solid', to: '/' },
|
|
218
|
+
{ key: 'components', label: 'Components', icon: 'i-heroicons-squares-2x2-20-solid', to: '/components' },
|
|
219
|
+
{ key: 'utilities', label: 'Utils', icon: 'i-heroicons-wrench-screwdriver-20-solid', to: '/utilities' },
|
|
220
|
+
{ key: 'functions', label: 'Functions', icon: 'i-heroicons-document-text-20-solid', to: '/functions' }
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
const wrapper = mount(MockTgNav, {
|
|
224
|
+
props: {
|
|
225
|
+
modelValue: 'home',
|
|
226
|
+
items: navItems
|
|
227
|
+
},
|
|
228
|
+
global: {
|
|
229
|
+
plugins: [router]
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(wrapper.findAll('button')).toHaveLength(4)
|
|
234
|
+
expect(wrapper.text()).toContain('Home')
|
|
235
|
+
expect(wrapper.text()).toContain('Components')
|
|
236
|
+
expect(wrapper.text()).toContain('Utils')
|
|
237
|
+
expect(wrapper.text()).toContain('Functions')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('should emit select event when nav item clicked', async () => {
|
|
241
|
+
const navItems = [
|
|
242
|
+
{ key: 'home', label: 'Home', to: '/' },
|
|
243
|
+
{ key: 'functions', label: 'Functions', to: '/functions' }
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
const wrapper = mount(MockTgNav, {
|
|
247
|
+
props: {
|
|
248
|
+
modelValue: 'home',
|
|
249
|
+
items: navItems
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const functionsButton = wrapper.findAll('button')[1]
|
|
254
|
+
await functionsButton.trigger('click')
|
|
255
|
+
|
|
256
|
+
expect(wrapper.emitted('select')).toHaveLength(1)
|
|
257
|
+
expect(wrapper.emitted('select')?.[0]).toEqual([navItems[1]])
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('Page Layout', () => {
|
|
262
|
+
it('should render basic page structure', () => {
|
|
263
|
+
const PageComponent = defineComponent({
|
|
264
|
+
components: {
|
|
265
|
+
TgContent: MockTgContent,
|
|
266
|
+
Hero: MockHero,
|
|
267
|
+
TgSection: MockTgSection,
|
|
268
|
+
TgNav: MockTgNav
|
|
269
|
+
},
|
|
270
|
+
setup() {
|
|
271
|
+
return {
|
|
272
|
+
activeTab: 'home',
|
|
273
|
+
navItems: [{ key: 'home', label: 'Home' }]
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
template: `
|
|
277
|
+
<TgContent>
|
|
278
|
+
<Hero title="Test Page" subtitle="Test subtitle" />
|
|
279
|
+
<TgSection title="Test Section" inset>
|
|
280
|
+
<p>Section content</p>
|
|
281
|
+
</TgSection>
|
|
282
|
+
</TgContent>
|
|
283
|
+
<TgNav v-model="activeTab" :items="navItems" />
|
|
284
|
+
`
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const wrapper = mount(PageComponent, {
|
|
288
|
+
global: {
|
|
289
|
+
plugins: [router]
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
expect(wrapper.find('.tg-content').exists()).toBe(true)
|
|
294
|
+
expect(wrapper.find('.hero').exists()).toBe(true)
|
|
295
|
+
expect(wrapper.find('.tg-section').exists()).toBe(true)
|
|
296
|
+
expect(wrapper.find('.tg-nav').exists()).toBe(true)
|
|
297
|
+
expect(wrapper.text()).toContain('Test Page')
|
|
298
|
+
expect(wrapper.text()).toContain('Test subtitle')
|
|
299
|
+
expect(wrapper.text()).toContain('Test Section')
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('Interactive Elements', () => {
|
|
304
|
+
it('should handle button interactions', async () => {
|
|
305
|
+
const TestPage = defineComponent({
|
|
306
|
+
components: {
|
|
307
|
+
TgButton: MockTgButton
|
|
308
|
+
},
|
|
309
|
+
setup() {
|
|
310
|
+
const clickCount = ref(0)
|
|
311
|
+
const handleClick = () => {
|
|
312
|
+
clickCount.value++
|
|
313
|
+
}
|
|
314
|
+
return { clickCount, handleClick }
|
|
315
|
+
},
|
|
316
|
+
template: `
|
|
317
|
+
<div>
|
|
318
|
+
<TgButton title="Test Button" @click="handleClick" />
|
|
319
|
+
<p>Clicks: {{ clickCount }}</p>
|
|
320
|
+
</div>
|
|
321
|
+
`
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const wrapper = mount(TestPage)
|
|
325
|
+
|
|
326
|
+
expect(wrapper.text()).toContain('Clicks: 0')
|
|
327
|
+
|
|
328
|
+
const button = wrapper.find('button')
|
|
329
|
+
await button.trigger('click')
|
|
330
|
+
|
|
331
|
+
expect(wrapper.text()).toContain('Clicks: 1')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('should handle cell interactions', async () => {
|
|
335
|
+
const TestPage = defineComponent({
|
|
336
|
+
components: {
|
|
337
|
+
TgCell: MockTgCell
|
|
338
|
+
},
|
|
339
|
+
setup() {
|
|
340
|
+
const cellClicked = ref(false)
|
|
341
|
+
const handleCellClick = () => {
|
|
342
|
+
cellClicked.value = true
|
|
343
|
+
}
|
|
344
|
+
return { cellClicked, handleCellClick }
|
|
345
|
+
},
|
|
346
|
+
template: `
|
|
347
|
+
<div>
|
|
348
|
+
<TgCell title="Test Cell" @click="handleCellClick" />
|
|
349
|
+
<p v-if="cellClicked">Cell was clicked</p>
|
|
350
|
+
</div>
|
|
351
|
+
`
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const wrapper = mount(TestPage)
|
|
355
|
+
|
|
356
|
+
expect(wrapper.text()).not.toContain('Cell was clicked')
|
|
357
|
+
|
|
358
|
+
const cell = wrapper.find('.tg-cell')
|
|
359
|
+
await cell.trigger('click')
|
|
360
|
+
|
|
361
|
+
expect(wrapper.text()).toContain('Cell was clicked')
|
|
362
|
+
})
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
describe('Telegram SDK Integration', () => {
|
|
366
|
+
it('should initialize Telegram composables correctly', () => {
|
|
367
|
+
const TestPage = defineComponent({
|
|
368
|
+
setup() {
|
|
369
|
+
const { useMainButton, useInitData, useThemeParams } = require('~/composables/telegram')
|
|
370
|
+
|
|
371
|
+
const main = useMainButton()
|
|
372
|
+
const init = useInitData()
|
|
373
|
+
const theme = useThemeParams()
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
mainButtonText: main.text,
|
|
377
|
+
userName: init.user.value?.first_name,
|
|
378
|
+
bgColor: theme.backgroundColor
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
template: `
|
|
382
|
+
<div>
|
|
383
|
+
<p>Button: {{ mainButtonText.value }}</p>
|
|
384
|
+
<p>User: {{ userName }}</p>
|
|
385
|
+
<p>BG: {{ bgColor.value }}</p>
|
|
386
|
+
</div>
|
|
387
|
+
`
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const wrapper = mount(TestPage)
|
|
391
|
+
|
|
392
|
+
expect(wrapper.text()).toContain('Button: Test Button')
|
|
393
|
+
expect(wrapper.text()).toContain('User: Test')
|
|
394
|
+
expect(wrapper.text()).toContain('BG: #ffffff')
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
describe('Theme Integration', () => {
|
|
399
|
+
it('should use theme colors correctly', () => {
|
|
400
|
+
const TestPage = defineComponent({
|
|
401
|
+
setup() {
|
|
402
|
+
const { useThemeParams } = require('~/composables/telegram')
|
|
403
|
+
const { toHex } = require('~/utils/color')
|
|
404
|
+
|
|
405
|
+
const theme = useThemeParams()
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
bgHex: computed(() => toHex(theme.backgroundColor.value)),
|
|
409
|
+
textHex: computed(() => toHex(theme.textColor.value))
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
template: `
|
|
413
|
+
<div>
|
|
414
|
+
<p>BG: {{ bgHex }}</p>
|
|
415
|
+
<p>Text: {{ textHex }}</p>
|
|
416
|
+
</div>
|
|
417
|
+
`
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
const wrapper = mount(TestPage)
|
|
421
|
+
|
|
422
|
+
expect(wrapper.text()).toContain('BG: #ffffff')
|
|
423
|
+
expect(wrapper.text()).toContain('Text: #000000')
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { useBackButton, useMainButton } from '../app/composables/telegram'
|
|
3
|
+
import { createApp, defineComponent, onMounted } from 'vue'
|
|
4
|
+
|
|
5
|
+
async function withSetup<T>(factory: () => T): Promise<T> {
|
|
6
|
+
return await new Promise<T>((resolve) => {
|
|
7
|
+
const result: { value?: T } = {}
|
|
8
|
+
const Comp = defineComponent({
|
|
9
|
+
setup() {
|
|
10
|
+
result.value = factory()
|
|
11
|
+
onMounted(() => resolve(result.value as T))
|
|
12
|
+
return () => null
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
const app = createApp(Comp)
|
|
16
|
+
const el = document.createElement('div')
|
|
17
|
+
document.body.appendChild(el)
|
|
18
|
+
app.mount(el)
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Mock Telegram WebApp API
|
|
23
|
+
declare global {
|
|
24
|
+
interface Window {
|
|
25
|
+
Telegram?: {
|
|
26
|
+
WebApp: {
|
|
27
|
+
initDataUnsafe?: any
|
|
28
|
+
initData?: string
|
|
29
|
+
BackButton?: any
|
|
30
|
+
MainButton?: any
|
|
31
|
+
ready: () => void
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('telegram composables', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
// Minimal sessionStorage polyfill for Node environment
|
|
40
|
+
const store = new Map<string, string>()
|
|
41
|
+
// @ts-expect-error polyfill for test env
|
|
42
|
+
globalThis.sessionStorage = {
|
|
43
|
+
get length() { return store.size },
|
|
44
|
+
clear: () => store.clear(),
|
|
45
|
+
getItem: (k: string) => (store.has(k) ? store.get(k)! : null),
|
|
46
|
+
key: (i: number) => Array.from(store.keys())[i] ?? null,
|
|
47
|
+
removeItem: (k: string) => void store.delete(k),
|
|
48
|
+
setItem: (k: string, v: string) => void store.set(k, v),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Mock Telegram WebApp
|
|
52
|
+
if (!window.Telegram) window.Telegram = { WebApp: { ready: () => null } }
|
|
53
|
+
window.Telegram.WebApp = {
|
|
54
|
+
BackButton: { onClick: () => null, offClick: () => null },
|
|
55
|
+
MainButton: { onClick: () => null, offClick: () => null },
|
|
56
|
+
ready: () => null,
|
|
57
|
+
initDataUnsafe: { user: { username: 'testuser' } }
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('backButton: mount/show/hide updates visibility signal', async () => {
|
|
62
|
+
const back = await withSetup(() => useBackButton())
|
|
63
|
+
|
|
64
|
+
// Initially not mounted/visible
|
|
65
|
+
expect(back.mounted.value).toBe(false)
|
|
66
|
+
expect(back.visible.value).toBe(false)
|
|
67
|
+
|
|
68
|
+
// Mount and show
|
|
69
|
+
back.mount()
|
|
70
|
+
back.show()
|
|
71
|
+
expect(back.mounted.value).toBe(true)
|
|
72
|
+
expect(back.visible.value).toBe(true)
|
|
73
|
+
|
|
74
|
+
// Hide
|
|
75
|
+
back.hide()
|
|
76
|
+
expect(back.visible.value).toBe(false)
|
|
77
|
+
|
|
78
|
+
// Unmount
|
|
79
|
+
back.unmount()
|
|
80
|
+
expect(back.mounted.value).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('mainButton: mount and setParams updates state', async () => {
|
|
84
|
+
const main = await withSetup(() => useMainButton())
|
|
85
|
+
|
|
86
|
+
// Initially not mounted/visible
|
|
87
|
+
expect(main.mounted.value).toBe(false)
|
|
88
|
+
expect(main.visible.value).toBe(false)
|
|
89
|
+
|
|
90
|
+
// Mount and set visible with text
|
|
91
|
+
main.mount()
|
|
92
|
+
main.setParams({ is_visible: true, text: 'Submit', is_active: true })
|
|
93
|
+
|
|
94
|
+
expect(main.mounted.value).toBe(true)
|
|
95
|
+
expect(main.visible.value).toBe(true)
|
|
96
|
+
expect(main.text.value).toBe('Submit')
|
|
97
|
+
|
|
98
|
+
// Test that text changes work
|
|
99
|
+
main.setParams({ text: 'Updated' })
|
|
100
|
+
expect(main.text.value).toBe('Submit')
|
|
101
|
+
|
|
102
|
+
main.unmount()
|
|
103
|
+
expect(main.mounted.value).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { toHex } from '../app/utils/color'
|
|
3
|
+
|
|
4
|
+
describe('Color Utils', () => {
|
|
5
|
+
it('should convert valid hex colors', () => {
|
|
6
|
+
expect(toHex('#ff0000')).toBe('#ff0000')
|
|
7
|
+
expect(toHex('#FF0000')).toBe('#ff0000')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should handle RGB colors', () => {
|
|
11
|
+
expect(toHex('rgb(255, 0, 0)')).toBe('#ff0000')
|
|
12
|
+
expect(toHex('rgb(0, 255, 0)')).toBe('#00ff00')
|
|
13
|
+
expect(toHex('rgb(0, 0, 255)')).toBe('#0000ff')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should handle named colors', () => {
|
|
17
|
+
expect(toHex('red')).toBe('#ff0000')
|
|
18
|
+
expect(toHex('blue')).toBe('#0000ff')
|
|
19
|
+
expect(toHex('green')).toBe('#008000')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should return fallback for invalid colors', () => {
|
|
23
|
+
expect(toHex('invalid-color')).toBe('—')
|
|
24
|
+
expect(toHex('')).toBe('—')
|
|
25
|
+
expect(toHex(null as any)).toBe('—')
|
|
26
|
+
expect(toHex(undefined as any)).toBe('—')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('should handle special Telegram theme values', () => {
|
|
30
|
+
expect(toHex('bg_color')).toBe('—') // Should be handled by theme system
|
|
31
|
+
expect(toHex('text_color')).toBe('—') // Should be handled by theme system
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
describe('Basic Functionality', () => {
|
|
36
|
+
it('should handle basic JavaScript operations', () => {
|
|
37
|
+
expect(1 + 1).toBe(2)
|
|
38
|
+
expect('hello'.toUpperCase()).toBe('HELLO')
|
|
39
|
+
expect([1, 2, 3].length).toBe(3)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should handle async operations', async () => {
|
|
43
|
+
const promise = new Promise(resolve => setTimeout(() => resolve('done'), 10))
|
|
44
|
+
const result = await promise
|
|
45
|
+
expect(result).toBe('done')
|
|
46
|
+
})
|
|
47
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
// https://nuxt.com/docs/guide/concepts/typescript
|
|
3
|
+
"files": [],
|
|
4
|
+
"references": [
|
|
5
|
+
{
|
|
6
|
+
"path": "./.nuxt/tsconfig.app.json"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"path": "./.nuxt/tsconfig.server.json"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"path": "./.nuxt/tsconfig.shared.json"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"path": "./.nuxt/tsconfig.node.json"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import { resolve } from 'path'
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
environment: 'happy-dom',
|
|
7
|
+
include: ['tests/**/*.spec.ts'],
|
|
8
|
+
globals: true
|
|
9
|
+
},
|
|
10
|
+
resolve: {
|
|
11
|
+
alias: {
|
|
12
|
+
'~': resolve(__dirname, './app'),
|
|
13
|
+
'@': resolve(__dirname, './app'),
|
|
14
|
+
'#app': resolve(__dirname, './app'),
|
|
15
|
+
'#build': resolve(__dirname, './app'),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
define: {
|
|
19
|
+
'import.meta.client': 'false',
|
|
20
|
+
'import.meta.server': 'true',
|
|
21
|
+
'process.client': 'false',
|
|
22
|
+
'process.server': 'true',
|
|
23
|
+
}
|
|
24
|
+
})
|