mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
354 lines
15 KiB
TypeScript
354 lines
15 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const source = readFileSync(
|
|
new URL('./MessagesApp.vue', import.meta.url),
|
|
'utf8',
|
|
)
|
|
const styles = readFileSync(
|
|
new URL('../../assets/main.css', import.meta.url),
|
|
'utf8',
|
|
)
|
|
|
|
describe('MessagesApp Sky UI contract', () => {
|
|
it('uses first-party Sky UI without direct Konsta markup', () => {
|
|
expect(source).not.toContain("from 'konsta/vue'")
|
|
expect(source).not.toMatch(/<\/?k-[a-z]/)
|
|
expect(source).toContain('<SkyAppPage')
|
|
expect(source).toContain('<SkyNavbar')
|
|
expect(source).toContain('<SkyScrollArea')
|
|
expect(source).toContain('<SkySearchbar')
|
|
expect(source).toContain('<SkyDropdown')
|
|
expect(source).toContain('<SkyFab')
|
|
expect(source).toContain('<SkyMessages')
|
|
expect(source).toContain('<SkyMessagebar')
|
|
expect(source).toContain('<SkyPillNavigation')
|
|
expect(source).toContain('<SkySettingsGroup')
|
|
expect(source).toContain('<SkySettingsRow')
|
|
expect(source).toContain('<SkySheet')
|
|
expect(source).toContain('<SkyToolbar')
|
|
expect(source).not.toContain('<SkySegmented')
|
|
})
|
|
|
|
it('uses the anchored central dropdown for sort, filter, and edit actions', () => {
|
|
const inboxStart = source.indexOf(
|
|
'class="messages-sky-page messages-sky-inbox"',
|
|
)
|
|
const composeStart = source.indexOf('v-else-if="composing"')
|
|
const inbox = source.slice(inboxStart, composeStart)
|
|
|
|
expect(source).toContain("id: 'sort-newest'")
|
|
expect(source).toContain("id: 'sort-oldest'")
|
|
expect(source).toContain("id: 'filter-all'")
|
|
expect(source).toContain("id: 'filter-unread'")
|
|
expect(source).toContain("id: 'edit'")
|
|
expect(source).toContain("phone.t('Apps.messages.sortNewest')")
|
|
expect(source).toContain("phone.t('Apps.messages.sortOldest')")
|
|
expect(source).toContain("phone.t('Apps.messages.allMessages')")
|
|
expect(source).toContain("phone.t('Apps.messages.unreadMessages')")
|
|
expect(source).toContain("phone.t('Common.edit')")
|
|
expect(source).toContain("group: 'sort'")
|
|
expect(source).toContain("group: 'filter'")
|
|
expect(source).toContain("phone.t('Apps.messages.sortLabel')")
|
|
expect(source).toContain("phone.t('Apps.messages.filterLabel')")
|
|
expect(source).toContain(
|
|
'inboxMenuTarget.value = event.currentTarget as HTMLElement',
|
|
)
|
|
expect(source).toContain("if (id === 'edit') toggleListEditing()")
|
|
|
|
expect(inbox).toContain('class="messages-sky-inbox-menu-trigger"')
|
|
expect(inbox).toContain('aria-haspopup="menu"')
|
|
expect(inbox).toContain(':aria-expanded="inboxMenuOpened"')
|
|
expect(inbox).toContain('@click="openInboxMenu"')
|
|
expect(inbox).toContain('id="messages-inbox-menu"')
|
|
expect(inbox).toContain(':items="inboxMenuItems"')
|
|
expect(inbox).toContain(':opened="inboxMenuOpened"')
|
|
expect(inbox).toContain(':target="inboxMenuTarget"')
|
|
expect(inbox).toContain('@backdropclick="dismissInboxMenu"')
|
|
expect(inbox).toContain('@escape="dismissInboxMenu"')
|
|
expect(inbox).toContain('@positionerror="dismissInboxMenu"')
|
|
expect(inbox).toContain('@select="selectInboxMenuItem"')
|
|
})
|
|
|
|
it('places search and the compose action together in the bottom toolbar', () => {
|
|
const inboxStart = source.indexOf(
|
|
'class="messages-sky-page messages-sky-inbox"',
|
|
)
|
|
const composeStart = source.indexOf('v-else-if="composing"')
|
|
const inbox = source.slice(inboxStart, composeStart)
|
|
const scrollEnd = inbox.indexOf('</SkyScrollArea>')
|
|
const toolbarStart = inbox.indexOf('<SkyToolbar')
|
|
const toolbarEnd = inbox.indexOf('</SkyToolbar>', toolbarStart)
|
|
const toolbar = inbox.slice(toolbarStart, toolbarEnd)
|
|
const searchbarStart = toolbar.indexOf('<SkySearchbar')
|
|
const fabStart = toolbar.indexOf('<SkyFab')
|
|
|
|
expect(toolbarStart).toBeGreaterThan(scrollEnd)
|
|
expect(toolbar).toContain('class="messages-sky-inbox-toolbar"')
|
|
expect(toolbar).toContain('component="footer"')
|
|
expect(searchbarStart).toBeGreaterThan(-1)
|
|
expect(fabStart).toBeGreaterThan(searchbarStart)
|
|
expect(toolbar).toContain('v-model="search"')
|
|
expect(toolbar).toContain('variant="glass"')
|
|
expect(toolbar).toContain('@click="beginCompose"')
|
|
expect(toolbar).toContain('<SquarePen :size="21" />')
|
|
expect(inbox).not.toContain('messages-sky-compose-navigation')
|
|
})
|
|
|
|
it('uses the results-empty state for search and unread filters', () => {
|
|
expect(source).toContain('search || showUnreadOnly')
|
|
expect(source).toContain('v-if="!search && !showUnreadOnly" #actions')
|
|
})
|
|
|
|
it('keeps the unread marker in the media slot left of the avatar', () => {
|
|
const inboxStart = source.indexOf(
|
|
'class="messages-sky-page messages-sky-inbox"',
|
|
)
|
|
const composeStart = source.indexOf('v-else-if="composing"')
|
|
const inbox = source.slice(inboxStart, composeStart)
|
|
const mediaSlotStart = inbox.indexOf('<template #media>')
|
|
const unreadSlot = inbox.indexOf(
|
|
'class="messages-sky-unread-slot"',
|
|
mediaSlotStart,
|
|
)
|
|
const avatar = inbox.indexOf('class="messages-avatar"', mediaSlotStart)
|
|
|
|
expect(mediaSlotStart).toBeGreaterThan(-1)
|
|
expect(unreadSlot).toBeGreaterThan(mediaSlotStart)
|
|
expect(avatar).toBeGreaterThan(unreadSlot)
|
|
expect(inbox).toContain(
|
|
"'is-visible': !editingList && conversation.unread > 0",
|
|
)
|
|
expect(source).toContain('count: String(conversation.unread)')
|
|
expect(inbox).toContain("'aria-pressed': selectedNumbers.includes(")
|
|
})
|
|
|
|
it('uses one compact scroll region and an in-flow composer in threads', () => {
|
|
expect(source).toContain('class="messages-sky-thread-scroll"')
|
|
expect(source).toContain('class="messages-bubbles"')
|
|
expect(source).toContain('class="messages-sky-composer-pill"')
|
|
expect(source).toContain('class="messages-sky-messagebar"')
|
|
expect(source).toMatch(
|
|
/\.messages-bubbles\s*\{[^}]*min-height:\s*100%[^}]*justify-content:\s*flex-end/s,
|
|
)
|
|
expect(source).toMatch(
|
|
/\.messages-sky-composer-shell\s*\{[^}]*flex:\s*none/s,
|
|
)
|
|
expect(source).toMatch(
|
|
/\.messages-sky-composer-pill\s*\{[^}]*border-radius:\s*var\(--sky-radius-pill\)/s,
|
|
)
|
|
})
|
|
|
|
it('keeps add and text entry in separate glass surfaces', () => {
|
|
expect(source).toContain('class="messages-sky-composer-row"')
|
|
expect(source).toMatch(
|
|
/<SkyGlass[\s\S]*?class="messages-sky-messagebar__action messages-sky-messagebar__plus"[\s\S]*?<\/SkyGlass>[\s\S]*?<SkyGlass[\s\S]*?class="messages-sky-composer-pill"/,
|
|
)
|
|
})
|
|
|
|
it('round-trips up to six media items into a removable composer preview', () => {
|
|
expect(source).toContain('const MAX_PENDING_ATTACHMENTS = 6')
|
|
expect(source).toContain(
|
|
'pendingAttachments: [...pendingAttachments.value]',
|
|
)
|
|
expect(source).toContain('messageMedia.consumeMany<MessagesMediaContext>')
|
|
expect(source).toContain('class="messages-pending-media"')
|
|
expect(source).toContain('@click="removePendingAttachment(media.id)"')
|
|
expect(source).toContain('Apps.photos.videoAlt')
|
|
expect(source).not.toMatch(/await sendAttachment\([\s\S]*?media\.mediaType/)
|
|
})
|
|
|
|
it('sends selected media in order and captions the last item', () => {
|
|
expect(source).toContain(
|
|
'for (const [index, media] of queuedAttachments.entries())',
|
|
)
|
|
expect(source).toContain(
|
|
'index === queuedAttachments.length - 1 ? body : undefined',
|
|
)
|
|
expect(source).toContain("media.mediaType === 'photo' ? 'image' : 'video'")
|
|
})
|
|
|
|
it('normalizes voice waveforms and uses the regular unfilled send icon', () => {
|
|
expect(source).toContain(
|
|
'compressWaveformSamples(recordingSamples, WAVEFORM_SAMPLES)',
|
|
)
|
|
expect(source).toContain('class="messages-recorder__send"')
|
|
expect(source).toContain('<ArrowUpCircle :size="27" :stroke-width="2.4" />')
|
|
expect(source).not.toContain('fill="currentColor" />\n </SkyButton>')
|
|
expect(source).toContain('const recordingStarting = ref(false)')
|
|
expect(source).toContain('if (requestId !== recordingRequestId)')
|
|
expect(source).toContain('requestedStream.getTracks().forEach')
|
|
expect(source).toContain(':disabled="sending || recordingStarting"')
|
|
})
|
|
|
|
it('uses provider dimensions for proportional GIF picker results', () => {
|
|
expect(source).toContain(
|
|
'aspectRatio: `${Math.max(1, gif.width)} / ${Math.max(1, gif.height)}`',
|
|
)
|
|
expect(source).toContain('const gifColumns = computed')
|
|
expect(source).toContain('class="messages-gif-grid"')
|
|
expect(source).toContain('class="messages-gif-column"')
|
|
expect(source).toContain('class="messages-gif-result"')
|
|
expect(styles).toMatch(
|
|
/\.messages-media-picker__gifs--masonry \.messages-gif-result img\s*\{[^}]*object-fit:\s*cover/s,
|
|
)
|
|
})
|
|
|
|
it('opens contact sharing in a draggable Sky UI bottom sheet', () => {
|
|
const sheetStart = source.indexOf('class="messages-media-picker-sheet"')
|
|
const sheetEnd = source.indexOf('</SkySheet>', sheetStart)
|
|
const sheet = source.slice(sheetStart, sheetEnd)
|
|
|
|
expect(sheetStart).toBeGreaterThan(-1)
|
|
expect(sheetEnd).toBeGreaterThan(sheetStart)
|
|
expect(sheet).toContain(
|
|
'activeCanMessage && !activeServiceLine && attachmentPicker !== null',
|
|
)
|
|
expect(sheet).toContain('swipe-to-close')
|
|
expect(sheet).toContain('grabber-clickable')
|
|
expect(sheet).toContain('@backdropclick="closeAttachmentPicker"')
|
|
expect(sheet).toContain('@escape="closeAttachmentPicker"')
|
|
expect(sheet).toContain('@grabberclick="closeAttachmentPicker"')
|
|
expect(sheet).toContain('@swipeclose="closeAttachmentPicker"')
|
|
expect(sheet).toContain('v-if="attachmentPicker === \'contacts\'"')
|
|
expect(sheet).toContain('@click="sendContact(contact)"')
|
|
expect(source).toContain('function closeAttachmentPicker(): void')
|
|
expect(styles).toMatch(
|
|
/\.messages-media-picker-sheet \.sky-sheet__panel\s*\{[^}]*border-radius:\s*30px 30px 0 0/s,
|
|
)
|
|
})
|
|
|
|
it('uses a surface back target in the thread and a compact recipient row', () => {
|
|
const threadStart = source.indexOf(
|
|
'class="messages-sky-page messages-sky-thread"',
|
|
)
|
|
const contactProfileStart = source.indexOf(
|
|
'v-if="contactDetailsOpen"',
|
|
threadStart,
|
|
)
|
|
const threadHeader = source.slice(threadStart, contactProfileStart)
|
|
|
|
expect(threadHeader).toContain('class="messages-sky-thread-navbar"')
|
|
expect(threadHeader).toContain('show-back')
|
|
expect(threadHeader).toContain('back-appearance="surface"')
|
|
expect(threadHeader).toContain('@back="goBack"')
|
|
expect(source).toContain('class="messages-recipient-field"')
|
|
expect(source).toContain('layout="inline"')
|
|
expect(source).toMatch(
|
|
/\.messages-recipient-field :deep\(\.sky-field\)\s*\{[^}]*min-height:\s*52px/s,
|
|
)
|
|
})
|
|
|
|
it('renders contact details read-only with icon actions and contact routing', () => {
|
|
const contactOverlayClass = source.indexOf(
|
|
'class="messages-contact-overlay"',
|
|
)
|
|
const contactProfileStart = source.lastIndexOf(
|
|
'<SkyAppPage',
|
|
contactOverlayClass,
|
|
)
|
|
const threadScrollStart = source.indexOf(
|
|
'class="messages-sky-thread-scroll"',
|
|
contactProfileStart,
|
|
)
|
|
const contactProfile = source.slice(contactProfileStart, threadScrollStart)
|
|
|
|
expect(contactProfileStart).toBeGreaterThan(-1)
|
|
expect(threadScrollStart).toBeGreaterThan(contactProfileStart)
|
|
expect(contactProfile).toContain('component="section"')
|
|
expect(contactProfile).toContain('back-appearance="surface"')
|
|
expect(contactProfile).toContain('activeContact?.avatar_url')
|
|
expect(contactProfile).toContain('<SkySettingsGroup')
|
|
expect(contactProfile).toContain('<SkySettingsRow')
|
|
expect(contactProfile).toContain('activeContact?.organization')
|
|
expect(contactProfile).toContain(':value="activeContact.organization"')
|
|
expect(contactProfile).toContain('activeContact?.notes')
|
|
expect(contactProfile).not.toContain('<SkyField')
|
|
|
|
expect(contactProfile).toMatch(
|
|
/<SkyButton\s+v-if="activeContact\?\.canCall !== false"\s+icon-only\s+rounded\s+tonal[\s\S]*?@click="callActiveContact"/,
|
|
)
|
|
expect(contactProfile).toMatch(
|
|
/<SkyButton\s+v-if="activeCanMessage"\s+icon-only\s+rounded\s+tonal[\s\S]*?@click="contactDetailsOpen = false"/,
|
|
)
|
|
expect(contactProfile).toMatch(
|
|
/<SkyButton\s+v-if="activeContactEmail"\s+icon-only\s+rounded\s+tonal[\s\S]*?@click="mailActiveContact"/,
|
|
)
|
|
expect(contactProfile).toContain('<PhoneIcon :size="22" />')
|
|
expect(contactProfile).toContain('<MessageCircle :size="22" />')
|
|
expect(contactProfile).toContain('<Mail :size="22" />')
|
|
|
|
expect(contactProfile).toContain('Apps.messages.showInContacts')
|
|
expect(contactProfile).toContain('Apps.messages.addContact')
|
|
expect(contactProfile).toContain('@activate="openActiveContactInPhone"')
|
|
expect(source).toContain("path: '/apps/phone'")
|
|
expect(source).toContain('{ contactId: activeContact.value.id }')
|
|
expect(source).toContain('{ newContactNumber: messages.activeNumber }')
|
|
expect(source).toContain("path: '/apps/mail'")
|
|
expect(source).toContain(
|
|
"query: { compose: '1', to: activeContactEmail.value }",
|
|
)
|
|
|
|
expect(contactProfile).not.toContain('Common.edit')
|
|
expect(source).not.toContain('contactEditing')
|
|
expect(source).not.toContain('saveContactDetails')
|
|
expect(source).not.toContain('deleteActiveContact')
|
|
expect(source).not.toContain('Apps.messages.deleteContact')
|
|
expect(contactProfile).not.toContain('<Pencil')
|
|
expect(source).toContain(':inert="contactDetailsOpen || undefined"')
|
|
expect(source).toContain(':aria-hidden="contactDetailsOpen"')
|
|
})
|
|
|
|
it('confirms contact blocking in a dismissible dialog', () => {
|
|
const contactOverlayClass = source.indexOf(
|
|
'class="messages-contact-overlay"',
|
|
)
|
|
const contactProfileStart = source.lastIndexOf(
|
|
'<SkyAppPage',
|
|
contactOverlayClass,
|
|
)
|
|
const threadScrollStart = source.indexOf(
|
|
'class="messages-sky-thread-scroll"',
|
|
contactProfileStart,
|
|
)
|
|
const contactProfile = source.slice(contactProfileStart, threadScrollStart)
|
|
const blockDialogOpened = source.indexOf(':opened="blockDialogOpened"')
|
|
const blockDialogStart = source.lastIndexOf('<SkyDialog', blockDialogOpened)
|
|
const notificationStart = source.indexOf(
|
|
'<SkyNotification',
|
|
blockDialogStart,
|
|
)
|
|
const blockDialog = source.slice(blockDialogStart, notificationStart)
|
|
|
|
expect(contactProfile).toContain('kind="action"')
|
|
expect(contactProfile).toContain('tone="danger"')
|
|
expect(contactProfile).toContain('Apps.messages.blockContact')
|
|
expect(contactProfile).toContain('@activate="confirmBlockActiveContact"')
|
|
expect(blockDialogStart).toBeGreaterThan(-1)
|
|
expect(blockDialog).toContain('@backdropclick="blockDialogOpened = false"')
|
|
expect(blockDialog).toContain('@escape="blockDialogOpened = false"')
|
|
expect(blockDialog).toContain('Apps.messages.blockContactTitle')
|
|
expect(blockDialog).toContain('Apps.messages.blockContactBody')
|
|
expect(blockDialog).toContain('@click="blockActiveContact"')
|
|
expect(source).toContain(
|
|
'const response = await calls.blockNumber(messages.activeNumber)',
|
|
)
|
|
})
|
|
|
|
it('keeps SMS conversations and recipients in flat iMessage-style lists', () => {
|
|
expect(source).not.toMatch(
|
|
/\.messages-sky-page\s*\{[^}]*--sky-page-gutter/s,
|
|
)
|
|
expect(source).toMatch(
|
|
/v-if="filteredConversations.length"\s+flush\s+class="messages-sky-conversation-list"/,
|
|
)
|
|
expect(source).toMatch(
|
|
/class="messages-recipient-field"\s+density="compact"\s+flush/,
|
|
)
|
|
expect(source).toMatch(
|
|
/v-if="contactSuggestions.length"\s+class="messages-contact-list"\s+flush/,
|
|
)
|
|
})
|
|
})
|