mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
1200 lines
34 KiB
JavaScript
1200 lines
34 KiB
JavaScript
const assert = require('node:assert/strict')
|
|
const { once } = require('node:events')
|
|
|
|
const { app } = require('./index.cjs')
|
|
|
|
const browserDataRequests = [
|
|
['development:bootstrap', {}],
|
|
['account:devices', {}],
|
|
['banking:overview', {}],
|
|
['crypto:bootstrap', {}],
|
|
['crypto:market-tick', {}],
|
|
['crypto:recipient', { walletKey: 'VX-DEAD-BEEF-C0DE-2026' }],
|
|
['crypto:quote', { marketId: 'aurora', quantity: '1', side: 'buy' }],
|
|
['health:overview', {}],
|
|
['billing:overview', {}],
|
|
['billing:list', { filter: 'all', limit: 20, offset: 0 }],
|
|
['calendar:list', { endsAt: 4_102_444_800, startsAt: 0 }],
|
|
['citywarn:bootstrap', {}],
|
|
['calls:recents', {}],
|
|
['companies:list', {}],
|
|
['companies:my-requests', { limit: 20, offset: 0 }],
|
|
['companies:work-context', {}],
|
|
['companies:work-queue', { limit: 20, offset: 0 }],
|
|
['contacts:list', {}],
|
|
['crewlink:login', { password: 'CrewLink123!' }],
|
|
['crewlink:bootstrap', {}],
|
|
['crewlink:live', {}],
|
|
['crewlink:nearby', {}],
|
|
['darkchat:bootstrap', {}],
|
|
['easyshare:bootstrap', {}],
|
|
['easyshare:own-contact', {}],
|
|
['feather:bootstrap', {}],
|
|
['feather:feed', { limit: 20 }],
|
|
['feather:explore', { limit: 20 }],
|
|
['flare:bootstrap', {}],
|
|
['fliptok:bootstrap', {}],
|
|
['fliptok:feed', { limit: 20 }],
|
|
['fliptok:discover', { limit: 20 }],
|
|
['fliptok:activities', {}],
|
|
['gallery:list', {}],
|
|
['gallery:counts', {}],
|
|
['garage:vehicles', {}],
|
|
['garage:valet-state', {}],
|
|
['housing:overview', {}],
|
|
['housing:key-candidates', { action: 'give' }],
|
|
['mail:counts', {}],
|
|
['mail:list', { folder: 'inbox' }],
|
|
['mail:mailboxes', {}],
|
|
['map:getPlayerCoords', {}],
|
|
['map:markers', {}],
|
|
['marketplace:counts', {}],
|
|
['marketplace:list', {}],
|
|
['marketplace:list-own', {}],
|
|
['marketplace:list-inquiries', {}],
|
|
['marketplace:profile', {}],
|
|
['messages:conversations', {}],
|
|
['messages:gifs', { query: 'party' }],
|
|
['memos:list', {}],
|
|
['music:bootstrap', {}],
|
|
['notes:list', {}],
|
|
['pages:list', {}],
|
|
['pages:list-own', {}],
|
|
['pages:profile', {}],
|
|
['picstagram:bootstrap', {}],
|
|
['picstagram:feed', { limit: 20 }],
|
|
['picstagram:explore', { limit: 20 }],
|
|
['picstagram:saved', {}],
|
|
['picstagram:stories', {}],
|
|
['picstagram:activities', {}],
|
|
['radio:get', {}],
|
|
['skyride:bootstrap', {}],
|
|
['skyride:history', {}],
|
|
['skyride:get-player-coords', {}],
|
|
['weather:get', {}],
|
|
['weazel-news:context', {}],
|
|
['weazel-news:list', { category: null, offset: 0, search: '' }],
|
|
['weazel-news:manage-list', { offset: 0, search: '', status: 'all' }],
|
|
]
|
|
|
|
async function post(baseUrl, endpoint, body = {}) {
|
|
const response = await fetch(`${baseUrl}/api/${endpoint}`, {
|
|
body: JSON.stringify(body),
|
|
headers: { 'Content-Type': 'application/json' },
|
|
method: 'POST',
|
|
})
|
|
assert.equal(response.status, 200, endpoint)
|
|
return response.json()
|
|
}
|
|
|
|
async function expectSuccess(baseUrl, endpoint, body = {}, data = false) {
|
|
const result = await post(baseUrl, endpoint, body)
|
|
assert.equal(result.success, true, `${endpoint}: ${result.error ?? 'failed'}`)
|
|
if (data) assert.notEqual(result.data, undefined, `${endpoint}: missing data`)
|
|
return result.data
|
|
}
|
|
|
|
function expectItems(value, label, minimum = 1) {
|
|
assert(
|
|
Array.isArray(value) && value.length >= minimum,
|
|
`${label} did not include enough browser test data`,
|
|
)
|
|
}
|
|
|
|
function verifyBrowserTestData(dataByEndpoint) {
|
|
const development = dataByEndpoint.get('development:bootstrap')
|
|
expectItems(development.device.data.alarms.payload, 'clock alarms', 3)
|
|
expectItems(
|
|
development.device.data.media.payload.captures,
|
|
'camera captures',
|
|
2,
|
|
)
|
|
expectItems(
|
|
development.device.data.apps.payload.claimedApps,
|
|
'installed demo apps',
|
|
41,
|
|
)
|
|
assert.equal(
|
|
new Set(development.device.data.apps.payload.claimedApps).size,
|
|
41,
|
|
'browser demo apps were not uniquely installed',
|
|
)
|
|
assert.equal(
|
|
development.device.data.settings.payload.settings.wallpaper,
|
|
'custom',
|
|
'browser demo home wallpaper was not configured',
|
|
)
|
|
assert.equal(
|
|
development.device.data.settings.payload.settings.lockWallpaper,
|
|
'custom',
|
|
'browser demo Lock Screen wallpaper was not configured',
|
|
)
|
|
assert.match(
|
|
development.device.data.settings.payload.settings.wallpaperImageUrl,
|
|
/sky-phone-demo-gradient\.png$/,
|
|
'browser demo wallpaper image was not configured',
|
|
)
|
|
expectItems(development.memos, 'memos', 3)
|
|
expectItems(development.notes, 'notes', 4)
|
|
assert(
|
|
Object.keys(development.device.data.games.payload).length >= 7,
|
|
'games did not include saved browser test progress',
|
|
)
|
|
|
|
expectItems(dataByEndpoint.get('health:overview').days, 'health history', 7)
|
|
const crypto = dataByEndpoint.get('crypto:bootstrap')
|
|
expectItems(crypto.markets, 'crypto markets', 24)
|
|
assert.equal(typeof crypto.profile.priceAlerts, 'boolean')
|
|
assert.match(crypto.profile.walletKey, /^VX-(?:[A-F0-9]{4}-){3}[A-F0-9]{4}$/)
|
|
assert.equal(typeof crypto.markets[0].issuedSupply, 'string')
|
|
assert(crypto.markets.every((market) => typeof market.logo === 'string'))
|
|
assert(
|
|
crypto.markets.every(
|
|
(market) =>
|
|
Array.isArray(market.priceHistory) &&
|
|
market.priceHistory.length >= 2 &&
|
|
market.priceHistory.at(-1) === Number(market.price).toFixed(2),
|
|
),
|
|
)
|
|
assert(
|
|
Math.max(...crypto.markets.map((market) => Number(market.price))) >=
|
|
1000000,
|
|
)
|
|
assert(
|
|
Math.min(...crypto.markets.map((market) => Number(market.price))) <= 0.01,
|
|
)
|
|
const cryptoTick = dataByEndpoint.get('crypto:market-tick')
|
|
expectItems(cryptoTick, 'live crypto market tick', 24)
|
|
assert(
|
|
cryptoTick.every(
|
|
(market) =>
|
|
typeof market.updatedAt === 'number' &&
|
|
market.priceHistory.at(-1) === market.price,
|
|
),
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('billing:list').invoices,
|
|
'billing invoices',
|
|
3,
|
|
)
|
|
expectItems(dataByEndpoint.get('calendar:list'), 'calendar events', 5)
|
|
const cityWarn = dataByEndpoint.get('citywarn:bootstrap')
|
|
expectItems(cityWarn.active, 'active CityWarn alerts', 2)
|
|
expectItems(cityWarn.archive, 'CityWarn alert history')
|
|
assert.equal(cityWarn.context.canPublish, true)
|
|
assert.deepEqual(
|
|
cityWarn.active.map((alert) => alert.title),
|
|
['Police operation in Mission Row', 'Water supply disruption'],
|
|
)
|
|
assert.equal(cityWarn.archive[0].title, 'City radio network restored')
|
|
expectItems(dataByEndpoint.get('calls:recents'), 'recent calls', 5)
|
|
expectItems(dataByEndpoint.get('companies:list').companies, 'companies', 3)
|
|
expectItems(dataByEndpoint.get('contacts:list'), 'contacts', 10)
|
|
expectItems(
|
|
dataByEndpoint.get('crewlink:bootstrap').groups,
|
|
'CrewLink groups',
|
|
2,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('darkchat:bootstrap').conversations,
|
|
'DarkChat conversations',
|
|
)
|
|
expectItems(dataByEndpoint.get('feather:feed').items, 'Feather posts', 5)
|
|
expectItems(
|
|
dataByEndpoint.get('flare:bootstrap').suggestions,
|
|
'Flare suggestions',
|
|
5,
|
|
)
|
|
expectItems(dataByEndpoint.get('fliptok:feed').items, 'FlipTok videos', 2)
|
|
expectItems(dataByEndpoint.get('gallery:list'), 'gallery media', 30)
|
|
expectItems(
|
|
dataByEndpoint.get('garage:vehicles').vehicles,
|
|
'garage vehicles',
|
|
4,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('housing:overview').properties,
|
|
'housing properties',
|
|
5,
|
|
)
|
|
expectItems(dataByEndpoint.get('mail:list').items, 'inbox mail', 2)
|
|
expectItems(
|
|
dataByEndpoint.get('mail:mailboxes').mailboxes,
|
|
'custom mailboxes',
|
|
)
|
|
expectItems(dataByEndpoint.get('map:markers'), 'map markers')
|
|
expectItems(
|
|
dataByEndpoint.get('marketplace:list').items,
|
|
'CityMarkt listings',
|
|
4,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('messages:conversations'),
|
|
'message conversations',
|
|
5,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('music:bootstrap').serverTracks,
|
|
'music tracks',
|
|
3,
|
|
)
|
|
expectItems(dataByEndpoint.get('pages:list').items, 'Local Pages posts', 4)
|
|
expectItems(
|
|
dataByEndpoint.get('picstagram:feed').items,
|
|
'Picstagram posts',
|
|
2,
|
|
)
|
|
expectItems(dataByEndpoint.get('radio:get').history, 'radio history', 2)
|
|
expectItems(dataByEndpoint.get('skyride:history').items, 'SkyRide history', 2)
|
|
expectItems(
|
|
dataByEndpoint.get('weazel-news:list').items,
|
|
'Weazel News articles',
|
|
5,
|
|
)
|
|
expectItems(
|
|
dataByEndpoint.get('weazel-news:manage-list').items,
|
|
'Weazel News editorial articles',
|
|
7,
|
|
)
|
|
}
|
|
|
|
async function verifyStatefulActions(baseUrl) {
|
|
const cryptoBeforeTransfer = await expectSuccess(
|
|
baseUrl,
|
|
'crypto:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
const auroraBeforeTransfer = cryptoBeforeTransfer.holdings.find(
|
|
(holding) => holding.assetId === 'aurora',
|
|
)
|
|
const cryptoAfterTransfer = await expectSuccess(
|
|
baseUrl,
|
|
'crypto:transfer',
|
|
{
|
|
idempotencyKey: 'smoke-transfer-1',
|
|
marketId: 'aurora',
|
|
password: 'VaultX123!',
|
|
quantity: '0.5',
|
|
walletKey: 'VX-DEAD-BEEF-C0DE-2026',
|
|
},
|
|
true,
|
|
)
|
|
const auroraAfterTransfer = cryptoAfterTransfer.holdings.find(
|
|
(holding) => holding.assetId === 'aurora',
|
|
)
|
|
assert.equal(
|
|
Number(auroraAfterTransfer.quantity),
|
|
Number(auroraBeforeTransfer.quantity) - 0.5,
|
|
)
|
|
assert.equal(cryptoAfterTransfer.activity[0].type, 'transfer_out')
|
|
assert.equal(
|
|
cryptoAfterTransfer.activity[0].counterpartyKey,
|
|
'VX-DEAD-BEEF-C0DE-2026',
|
|
)
|
|
|
|
const companyCall = await expectSuccess(
|
|
baseUrl,
|
|
'companies:dial-service-line',
|
|
{ phoneNumber: '5551110001' },
|
|
true,
|
|
)
|
|
assert.equal(companyCall.direction, 'outgoing')
|
|
assert.equal(companyCall.otherNumber, '5551110001')
|
|
assert.equal(companyCall.state, 'ringing')
|
|
|
|
let gallery = await expectSuccess(baseUrl, 'gallery:list', {}, true)
|
|
assert(gallery.length >= 10, 'gallery:list did not include enough test media')
|
|
assert(
|
|
gallery.filter((item) => item.mediaType === 'video').length >= 3,
|
|
'gallery:list did not include enough test videos',
|
|
)
|
|
assert(
|
|
gallery
|
|
.filter((item) => item.mediaType === 'video')
|
|
.every((item) => typeof item.thumbnailUrl === 'string'),
|
|
'gallery:list returned a test video without a thumbnail',
|
|
)
|
|
const flipTokPhotos = gallery
|
|
.filter((item) => item.mediaType === 'photo')
|
|
.slice(0, 3)
|
|
const createdFlipTok = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:publish',
|
|
{
|
|
caption: 'Browser mock photo slideshow',
|
|
commentsEnabled: true,
|
|
coverTimeMs: 0,
|
|
customMusicUrl: '',
|
|
draft: false,
|
|
mediaId: flipTokPhotos[0].id,
|
|
mediaIds: flipTokPhotos.map((item) => item.id),
|
|
mediaType: 'photo',
|
|
musicTrack: '',
|
|
musicVolume: 0,
|
|
originalVolume: 0,
|
|
trimEndMs: null,
|
|
trimStartMs: 0,
|
|
visibility: 'public',
|
|
},
|
|
true,
|
|
)
|
|
let flipTokFeed = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:feed',
|
|
{ mode: 'for-you', offset: 0 },
|
|
true,
|
|
)
|
|
const photoFlipTok = flipTokFeed.items.find(
|
|
(item) => item.id === createdFlipTok.id,
|
|
)
|
|
assert.equal(photoFlipTok.media_type, 'photo')
|
|
assert.deepEqual(
|
|
photoFlipTok.media.map((item) => item.id),
|
|
flipTokPhotos.map((item) => item.id),
|
|
'fliptok:publish did not preserve photo order',
|
|
)
|
|
await expectSuccess(baseUrl, 'fliptok:comment', {
|
|
body: 'Browser mock scoped comment',
|
|
id: createdFlipTok.id,
|
|
})
|
|
const createdComments = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:comments',
|
|
{ id: createdFlipTok.id },
|
|
true,
|
|
)
|
|
assert(
|
|
createdComments.some((comment) => comment.body.includes('scoped comment')),
|
|
'fliptok:comment did not persist for its video',
|
|
)
|
|
const otherComments = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:comments',
|
|
{ id: 'fliptok-1' },
|
|
true,
|
|
)
|
|
assert(
|
|
!otherComments.some((comment) => comment.body.includes('scoped comment')),
|
|
'fliptok:comments leaked comments from another video',
|
|
)
|
|
await expectSuccess(baseUrl, 'fliptok:delete', { id: createdFlipTok.id })
|
|
flipTokFeed = await expectSuccess(
|
|
baseUrl,
|
|
'fliptok:feed',
|
|
{ mode: 'for-you', offset: 0 },
|
|
true,
|
|
)
|
|
assert(
|
|
!flipTokFeed.items.some((item) => item.id === createdFlipTok.id),
|
|
'fliptok:delete kept the removed video in the feed',
|
|
)
|
|
await expectSuccess(
|
|
baseUrl,
|
|
'gallery:favorite',
|
|
{ favorite: true, id: gallery[0].id },
|
|
true,
|
|
)
|
|
gallery = await expectSuccess(baseUrl, 'gallery:list', {}, true)
|
|
assert.equal(
|
|
gallery.find((item) => item.id === gallery[0].id)?.favorite,
|
|
true,
|
|
)
|
|
const articlePhotos = gallery
|
|
.filter((item) => item.mediaType === 'photo')
|
|
.slice(0, 7)
|
|
assert.equal(
|
|
articlePhotos.length,
|
|
7,
|
|
'gallery:list did not include enough photos for Weazel News',
|
|
)
|
|
const weazelContext = await expectSuccess(
|
|
baseUrl,
|
|
'weazel-news:context',
|
|
{},
|
|
true,
|
|
)
|
|
assert.equal(weazelContext.maximumImages, 6)
|
|
const createdArticleResponse = await expectSuccess(
|
|
baseUrl,
|
|
'weazel-news:create',
|
|
{
|
|
body: 'Created by the browser mock smoke test with several photos.',
|
|
category: 'news',
|
|
imageMediaIds: articlePhotos.slice(0, 3).map((item) => item.id),
|
|
status: 'published',
|
|
title: 'Browser test Weazel article',
|
|
},
|
|
true,
|
|
)
|
|
const createdArticle = createdArticleResponse.article
|
|
assert.deepEqual(
|
|
createdArticle.images.map((image) => image.mediaId),
|
|
articlePhotos.slice(0, 3).map((item) => item.id),
|
|
'weazel-news:create did not preserve image order',
|
|
)
|
|
assert.equal(createdArticle.imageMediaId, articlePhotos[0].id)
|
|
|
|
const updatedArticleResponse = await expectSuccess(
|
|
baseUrl,
|
|
'weazel-news:update',
|
|
{
|
|
body: createdArticle.body,
|
|
category: 'business',
|
|
id: createdArticle.id,
|
|
imageMediaIds: [articlePhotos[2].id, articlePhotos[0].id],
|
|
revision: createdArticle.revision,
|
|
status: 'draft',
|
|
title: 'Updated browser test Weazel article',
|
|
},
|
|
true,
|
|
)
|
|
const updatedArticle = updatedArticleResponse.article
|
|
assert.deepEqual(
|
|
updatedArticle.images.map((image) => image.mediaId),
|
|
[articlePhotos[2].id, articlePhotos[0].id],
|
|
'weazel-news:update did not preserve the reordered images',
|
|
)
|
|
assert.equal(updatedArticle.imageMediaId, articlePhotos[2].id)
|
|
|
|
const loadedArticleResponse = await expectSuccess(
|
|
baseUrl,
|
|
'weazel-news:get',
|
|
{ id: updatedArticle.id, manage: true },
|
|
true,
|
|
)
|
|
assert.deepEqual(loadedArticleResponse.article.images, updatedArticle.images)
|
|
|
|
const tooManyImages = await post(baseUrl, 'weazel-news:create', {
|
|
body: 'This article must be rejected because it has too many photos.',
|
|
category: 'news',
|
|
imageMediaIds: articlePhotos.map((item) => item.id),
|
|
status: 'draft',
|
|
title: 'Invalid Weazel article',
|
|
})
|
|
assert.equal(tooManyImages.success, false)
|
|
assert.equal(tooManyImages.error, 'invalid_attachment')
|
|
|
|
const firstSmsPhoto = await expectSuccess(
|
|
baseUrl,
|
|
'messages:send',
|
|
{
|
|
body: '',
|
|
mediaAssetId: String(articlePhotos[0].id),
|
|
messageType: 'image',
|
|
phoneNumber: '5558675309',
|
|
},
|
|
true,
|
|
)
|
|
const captionedSmsPhoto = await expectSuccess(
|
|
baseUrl,
|
|
'messages:send',
|
|
{
|
|
body: 'Two photos from one composer draft.',
|
|
mediaAssetId: String(articlePhotos[1].id),
|
|
messageType: 'image',
|
|
phoneNumber: '5558675309',
|
|
},
|
|
true,
|
|
)
|
|
assert.notEqual(firstSmsPhoto.id, captionedSmsPhoto.id)
|
|
assert.equal(captionedSmsPhoto.body, 'Two photos from one composer draft.')
|
|
const smsPhotoThread = await expectSuccess(
|
|
baseUrl,
|
|
'messages:thread',
|
|
{ phoneNumber: '5558675309' },
|
|
true,
|
|
)
|
|
assert(smsPhotoThread.some((message) => message.id === firstSmsPhoto.id))
|
|
assert(smsPhotoThread.some((message) => message.id === captionedSmsPhoto.id))
|
|
|
|
await expectSuccess(baseUrl, 'weazel-news:delete', {
|
|
id: updatedArticle.id,
|
|
revision: updatedArticle.revision,
|
|
})
|
|
|
|
const memoBootstrap = await expectSuccess(
|
|
baseUrl,
|
|
'development:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert(
|
|
Array.isArray(memoBootstrap.memos) && memoBootstrap.memos.length >= 3,
|
|
'development:bootstrap did not include realistic memo data',
|
|
)
|
|
assert(
|
|
memoBootstrap.memos.every(
|
|
(memo) =>
|
|
typeof memo.url === 'string' &&
|
|
memo.url.length > 0 &&
|
|
Array.isArray(memo.waveform) &&
|
|
memo.waveform.length >= 8,
|
|
),
|
|
'development:bootstrap returned an invalid memo response shape',
|
|
)
|
|
|
|
let memos = await expectSuccess(baseUrl, 'memos:list', {}, true)
|
|
const capturedMemo = await expectSuccess(
|
|
baseUrl,
|
|
'memos:devCapture',
|
|
{
|
|
audioDataUrl: 'data:audio/webm;base64,T2dnUw==',
|
|
correlationId: 'browser-smoke-memo',
|
|
durationMs: 1_250,
|
|
mimeType: 'audio/webm;codecs=opus',
|
|
note: 'Recorded in the browser preview.',
|
|
pinned: false,
|
|
title: 'Browser recording',
|
|
waveform: Array(16).fill(0.35),
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(capturedMemo.title, 'Browser recording')
|
|
assert.equal(capturedMemo.mimeType, 'audio/webm')
|
|
assert.equal(capturedMemo.sizeBytes, 4)
|
|
assert.match(capturedMemo.id, /^[0-9a-f-]{36}$/)
|
|
memos = await expectSuccess(baseUrl, 'memos:list', {}, true)
|
|
assert(
|
|
memos.some((item) => item.id === capturedMemo.id),
|
|
'memos:devCapture did not persist',
|
|
)
|
|
await expectSuccess(baseUrl, 'memos:delete', { id: capturedMemo.id }, true)
|
|
memos = await expectSuccess(baseUrl, 'memos:list', {}, true)
|
|
|
|
const memo = memos.find((item) => !item.pinned)
|
|
assert(memo, 'memos:list did not return an editable memo')
|
|
const updatedMemo = await expectSuccess(
|
|
baseUrl,
|
|
'memos:update',
|
|
{
|
|
id: memo.id,
|
|
note: 'Updated by the browser mock smoke test.',
|
|
pinned: true,
|
|
revision: memo.revision,
|
|
title: 'Updated browser memo',
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(updatedMemo.id, memo.id)
|
|
assert.equal(updatedMemo.title, 'Updated browser memo')
|
|
assert.equal(updatedMemo.note, 'Updated by the browser mock smoke test.')
|
|
assert.equal(updatedMemo.pinned, true)
|
|
assert.equal(updatedMemo.revision, memo.revision + 1)
|
|
assert(Array.isArray(updatedMemo.waveform) && updatedMemo.waveform.length > 0)
|
|
|
|
const deletedMemo = await expectSuccess(
|
|
baseUrl,
|
|
'memos:delete',
|
|
{ id: updatedMemo.id },
|
|
true,
|
|
)
|
|
assert.deepEqual(deletedMemo, { id: updatedMemo.id })
|
|
memos = await expectSuccess(baseUrl, 'memos:list', {}, true)
|
|
assert(
|
|
!memos.some((item) => item.id === updatedMemo.id),
|
|
'memos:delete did not persist',
|
|
)
|
|
|
|
const noteId = `browser-note-${Date.now()}`
|
|
let notes = await expectSuccess(
|
|
baseUrl,
|
|
'notes:create',
|
|
{
|
|
body: 'Created by the browser mock smoke test.',
|
|
id: noteId,
|
|
title: 'Browser test',
|
|
},
|
|
true,
|
|
)
|
|
assert(
|
|
notes.some((note) => note.id === noteId),
|
|
'notes:create did not persist',
|
|
)
|
|
notes = await expectSuccess(
|
|
baseUrl,
|
|
'notes:update',
|
|
{
|
|
body: 'Updated browser test note.',
|
|
id: noteId,
|
|
title: 'Browser test updated',
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(
|
|
notes.find((note) => note.id === noteId)?.title,
|
|
'Browser test updated',
|
|
)
|
|
notes = await expectSuccess(baseUrl, 'notes:delete', { id: noteId }, true)
|
|
assert(
|
|
!notes.some((note) => note.id === noteId),
|
|
'notes:delete did not persist',
|
|
)
|
|
|
|
const contact = await expectSuccess(
|
|
baseUrl,
|
|
'contacts:save',
|
|
{
|
|
email: 'browser.tester',
|
|
name: 'Browser Tester',
|
|
phoneNumber: '5552223333',
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(contact.email, 'browser.tester@ifruit.com')
|
|
await expectSuccess(
|
|
baseUrl,
|
|
'contacts:favorite',
|
|
{ favorite: true, id: contact.id },
|
|
true,
|
|
)
|
|
let contacts = await expectSuccess(baseUrl, 'contacts:list', {}, true)
|
|
const savedContact = contacts.find((item) => item.id === contact.id)
|
|
assert.equal(savedContact?.favorite, true)
|
|
assert.equal(savedContact?.email, 'browser.tester@ifruit.com')
|
|
await expectSuccess(baseUrl, 'contacts:delete', { id: contact.id })
|
|
contacts = await expectSuccess(baseUrl, 'contacts:list', {}, true)
|
|
assert(
|
|
!contacts.some((item) => item.id === contact.id),
|
|
'contacts:delete did not persist',
|
|
)
|
|
|
|
const event = await expectSuccess(
|
|
baseUrl,
|
|
'calendar:create',
|
|
{
|
|
allDay: false,
|
|
description: 'Stateful browser mock check',
|
|
endsAt: 2_000_003_600,
|
|
location: 'Legion Square',
|
|
reminderMinutes: 15,
|
|
startsAt: 2_000_000_000,
|
|
title: 'Browser test event',
|
|
},
|
|
true,
|
|
)
|
|
let events = await expectSuccess(
|
|
baseUrl,
|
|
'calendar:list',
|
|
{ endsAt: 4_102_444_800, startsAt: 0 },
|
|
true,
|
|
)
|
|
const storedEvent = events.find((item) => item.id === event.id)
|
|
assert(storedEvent, 'calendar:create did not persist')
|
|
await expectSuccess(baseUrl, 'calendar:update', {
|
|
...storedEvent,
|
|
endsAt: storedEvent.endsAt / 1000,
|
|
startsAt: storedEvent.startsAt / 1000,
|
|
title: 'Updated browser test event',
|
|
})
|
|
events = await expectSuccess(
|
|
baseUrl,
|
|
'calendar:list',
|
|
{ endsAt: 4_102_444_800, startsAt: 0 },
|
|
true,
|
|
)
|
|
assert.equal(
|
|
events.find((item) => item.id === event.id)?.title,
|
|
'Updated browser test event',
|
|
)
|
|
await expectSuccess(baseUrl, 'calendar:delete', { id: event.id })
|
|
|
|
const marker = await expectSuccess(
|
|
baseUrl,
|
|
'map:create-marker',
|
|
{
|
|
color: '#2dd4bf',
|
|
coords: { x: 215.2, y: -810.1, z: 30.7 },
|
|
icon: 'pin',
|
|
label: 'Browser test marker',
|
|
},
|
|
true,
|
|
)
|
|
let markers = await expectSuccess(baseUrl, 'map:markers', {}, true)
|
|
assert(
|
|
markers.some((item) => item.id === marker.id),
|
|
'map:create-marker did not persist',
|
|
)
|
|
await expectSuccess(baseUrl, 'map:delete-marker', { id: marker.id })
|
|
markers = await expectSuccess(baseUrl, 'map:markers', {}, true)
|
|
assert(
|
|
!markers.some((item) => item.id === marker.id),
|
|
'map:delete-marker did not persist',
|
|
)
|
|
|
|
const bankingBefore = await expectSuccess(
|
|
baseUrl,
|
|
'banking:overview',
|
|
{},
|
|
true,
|
|
)
|
|
const bankingAfter = await expectSuccess(
|
|
baseUrl,
|
|
'banking:transfer',
|
|
{ amount: 125, phoneNumber: '5551110001' },
|
|
true,
|
|
)
|
|
assert.equal(bankingAfter.bank, bankingBefore.bank - 125)
|
|
|
|
const health = await expectSuccess(baseUrl, 'health:overview', {}, true)
|
|
assert.equal(health.dailyStepGoal, 8000)
|
|
assert.equal(health.days.length, 7)
|
|
assert.equal(health.days.at(-1).steps, 6420)
|
|
assert.equal('snapshot' in health, false)
|
|
|
|
const medicalId = await expectSuccess(
|
|
baseUrl,
|
|
'health:save-profile',
|
|
{
|
|
allergies: 'Penicillin',
|
|
bloodType: 'A+',
|
|
conditions: 'Asthma',
|
|
emergencyName: 'Jamie Morgan',
|
|
emergencyPhone: '5550102211',
|
|
emergencyRelation: 'Sibling',
|
|
medication: 'Inhaler',
|
|
},
|
|
true,
|
|
)
|
|
assert.equal(medicalId.bloodType, 'A+')
|
|
assert.equal(medicalId.allergies, 'Penicillin')
|
|
|
|
let radio = await expectSuccess(
|
|
baseUrl,
|
|
'radio:connect',
|
|
{ frequency: 42.5, secondaryFrequency: 7.25 },
|
|
true,
|
|
)
|
|
assert.equal(radio.frequency, 42.5)
|
|
radio = await expectSuccess(baseUrl, 'radio:set-volume', { volume: 44 }, true)
|
|
assert.equal(radio.volume, 44)
|
|
radio = await expectSuccess(
|
|
baseUrl,
|
|
'radio:set-speaker',
|
|
{ enabled: true },
|
|
true,
|
|
)
|
|
assert.equal(radio.speakerEnabled, true)
|
|
await expectSuccess(baseUrl, 'radio:disconnect')
|
|
|
|
const playlistState = await expectSuccess(
|
|
baseUrl,
|
|
'music:create-playlist',
|
|
{ name: 'Browser Test Mix' },
|
|
true,
|
|
)
|
|
const playlist = playlistState.playlists.find(
|
|
(item) => item.name === 'Browser Test Mix',
|
|
)
|
|
assert(playlist, 'music:create-playlist did not persist')
|
|
await expectSuccess(
|
|
baseUrl,
|
|
'music:rename-playlist',
|
|
{ id: playlist.id, name: 'Updated Browser Mix' },
|
|
true,
|
|
)
|
|
await expectSuccess(
|
|
baseUrl,
|
|
'music:delete-playlist',
|
|
{ id: playlist.id },
|
|
true,
|
|
)
|
|
|
|
await expectSuccess(baseUrl, 'sim:eject')
|
|
let bootstrap = await expectSuccess(
|
|
baseUrl,
|
|
'development:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert.equal(bootstrap.device.sim, null)
|
|
const simConfirmation = await post(baseUrl, 'sim:insert', {
|
|
imei: '356938035643810',
|
|
})
|
|
assert.deepEqual(simConfirmation, {
|
|
error: 'confirmation_required',
|
|
success: false,
|
|
})
|
|
await expectSuccess(baseUrl, 'sim:insert', {
|
|
confirmed: true,
|
|
imei: '356938035643810',
|
|
})
|
|
bootstrap = await expectSuccess(baseUrl, 'development:bootstrap', {}, true)
|
|
assert.equal(bootstrap.device.sim.number, '5559876543')
|
|
|
|
const payphoneCall = await expectSuccess(
|
|
baseUrl,
|
|
'payphone:dial',
|
|
{ phoneNumber: '5551110001' },
|
|
true,
|
|
)
|
|
assert.equal(payphoneCall.state, 'connected')
|
|
await expectSuccess(baseUrl, 'payphone:hangup')
|
|
|
|
const mailbox = await expectSuccess(
|
|
baseUrl,
|
|
'mail:create-mailbox',
|
|
{ name: 'Browser Test' },
|
|
true,
|
|
)
|
|
const mailboxes = await expectSuccess(baseUrl, 'mail:mailboxes', {}, true)
|
|
assert(
|
|
mailboxes.mailboxes.some((item) => item.id === mailbox.id),
|
|
'mail:create-mailbox did not update the mock mailbox list',
|
|
)
|
|
await expectSuccess(baseUrl, 'mail:move', {
|
|
id: 2,
|
|
mailboxId: mailbox.id,
|
|
})
|
|
const mailboxItems = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: `mailbox:${mailbox.id}` },
|
|
true,
|
|
)
|
|
assert(
|
|
mailboxItems.items.some((message) => message.id === 2),
|
|
'mail:move did not add the message to the custom mailbox',
|
|
)
|
|
await expectSuccess(baseUrl, 'mail:move', { id: 2, mailboxId: 0 })
|
|
await expectSuccess(baseUrl, 'mail:delete-mailbox', { id: mailbox.id })
|
|
|
|
const draft = await expectSuccess(
|
|
baseUrl,
|
|
'mail:save-draft',
|
|
{
|
|
body: 'Browser test body',
|
|
recipients: ['alex@ifruit.com'],
|
|
subject: 'Browser test mail',
|
|
},
|
|
true,
|
|
)
|
|
const storedDraft = await expectSuccess(
|
|
baseUrl,
|
|
'mail:get-draft',
|
|
{ id: draft.id },
|
|
true,
|
|
)
|
|
assert.equal(storedDraft.subject, 'Browser test mail')
|
|
await expectSuccess(baseUrl, 'mail:delete-many', {
|
|
folder: 'drafts',
|
|
ids: [draft.id],
|
|
})
|
|
const drafts = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'drafts' },
|
|
true,
|
|
)
|
|
assert.equal(drafts.items.length, 0, 'mail:delete-many kept selected draft')
|
|
|
|
await expectSuccess(baseUrl, 'mail:delete-many', {
|
|
folder: 'inbox',
|
|
ids: [1],
|
|
})
|
|
const inbox = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'inbox' },
|
|
true,
|
|
)
|
|
assert(
|
|
inbox.items.some((message) => message.id === 2),
|
|
'mail:delete-many removed a non-selected inbox message',
|
|
)
|
|
const trash = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'trash' },
|
|
true,
|
|
)
|
|
assert(
|
|
trash.items.some((message) => message.id === 1),
|
|
'mail:delete-many did not move selected inbox mail to trash',
|
|
)
|
|
|
|
await expectSuccess(baseUrl, 'mail:delete-many', {
|
|
folder: 'sent',
|
|
ids: [3],
|
|
})
|
|
const sent = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'sent' },
|
|
true,
|
|
)
|
|
assert.equal(sent.items.length, 0, 'mail:delete-many kept selected sent mail')
|
|
|
|
await expectSuccess(baseUrl, 'mail:delete-many', {
|
|
folder: 'trash',
|
|
ids: [1],
|
|
})
|
|
const remainingTrash = await expectSuccess(
|
|
baseUrl,
|
|
'mail:list',
|
|
{ folder: 'trash' },
|
|
true,
|
|
)
|
|
assert(
|
|
!remainingTrash.items.some((message) => message.id === 1),
|
|
'mail:delete-many kept permanently deleted trash mail',
|
|
)
|
|
assert(
|
|
remainingTrash.items.some((message) => message.id === 4),
|
|
'mail:delete-many removed non-selected trash mail',
|
|
)
|
|
|
|
const flareBeforeDelete = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert(flareBeforeDelete.profile, 'flare:bootstrap did not include a profile')
|
|
assert(
|
|
flareBeforeDelete.profile.photoMediaIds.length >= 1 &&
|
|
flareBeforeDelete.profile.photoMediaIds.length <= 6,
|
|
'flare:bootstrap profile did not include one to six gallery photos',
|
|
)
|
|
assert.equal(
|
|
flareBeforeDelete.profile.photoMediaIds.length,
|
|
flareBeforeDelete.profile.photoUrls.length,
|
|
'flare:bootstrap profile photo IDs and URLs were out of sync',
|
|
)
|
|
const galleryPhotoUrls = new Set(
|
|
gallery
|
|
.filter((item) => item.mediaType === 'photo')
|
|
.map((item) => item.url),
|
|
)
|
|
assert(
|
|
flareBeforeDelete.profile.photoUrls.every((url) =>
|
|
galleryPhotoUrls.has(url),
|
|
),
|
|
'flare:bootstrap profile used photos outside the phone gallery',
|
|
)
|
|
assert(
|
|
flareBeforeDelete.suggestions.every(
|
|
(profile) =>
|
|
profile.photoUrls.length >= 1 &&
|
|
profile.photoUrls.every((url) => galleryPhotoUrls.has(url)),
|
|
),
|
|
'Flare suggestions used photos outside the phone gallery',
|
|
)
|
|
assert(
|
|
flareBeforeDelete.matches.length > 0,
|
|
'flare:bootstrap did not include a deletable match',
|
|
)
|
|
assert(
|
|
flareBeforeDelete.matches.every(
|
|
(match) =>
|
|
match.profile.photoUrls.length >= 1 &&
|
|
match.profile.photoUrls.every((url) => galleryPhotoUrls.has(url)),
|
|
),
|
|
'Flare matches used photos outside the phone gallery',
|
|
)
|
|
const [removedProfilePhotoId, retainedProfilePhotoId] =
|
|
flareBeforeDelete.profile.photoMediaIds
|
|
await expectSuccess(baseUrl, 'gallery:delete', {
|
|
id: removedProfilePhotoId,
|
|
})
|
|
const flareAfterGalleryDelete = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert.deepEqual(
|
|
flareAfterGalleryDelete.profile.photoMediaIds,
|
|
[retainedProfilePhotoId],
|
|
'gallery:delete did not remove the deleted photo from the Flare profile',
|
|
)
|
|
assert.deepEqual(
|
|
flareAfterGalleryDelete.profile.photoUrls,
|
|
[gallery.find((item) => item.id === retainedProfilePhotoId)?.url],
|
|
'gallery:delete left Flare photo IDs and URLs out of sync',
|
|
)
|
|
const rejectedLastPhotoDelete = await post(baseUrl, 'gallery:delete', {
|
|
id: retainedProfilePhotoId,
|
|
})
|
|
assert.deepEqual(rejectedLastPhotoDelete, {
|
|
error: 'profile_photo_required',
|
|
success: false,
|
|
})
|
|
const nonProfilePhotoId = gallery.find(
|
|
(item) =>
|
|
item.mediaType === 'photo' &&
|
|
!flareBeforeDelete.profile.photoMediaIds.includes(item.id),
|
|
)?.id
|
|
const rejectedBulkLastPhotoDelete = await post(
|
|
baseUrl,
|
|
'gallery:delete-many',
|
|
{
|
|
correlationId: 'flare-last-photo-protection',
|
|
ids: [retainedProfilePhotoId, nonProfilePhotoId],
|
|
},
|
|
)
|
|
assert.deepEqual(rejectedBulkLastPhotoDelete, {
|
|
error: 'profile_photo_required',
|
|
success: false,
|
|
})
|
|
const swipedProfile = flareBeforeDelete.suggestions[0]
|
|
await expectSuccess(baseUrl, 'flare:swipe', {
|
|
choice: 'pass',
|
|
targetId: swipedProfile.id,
|
|
})
|
|
const flareAfterSwipe = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert(
|
|
!flareAfterSwipe.suggestions.some(
|
|
(profile) => profile.id === swipedProfile.id,
|
|
),
|
|
'flare:swipe did not filter the swiped profile',
|
|
)
|
|
await expectSuccess(baseUrl, 'flare:delete-profile')
|
|
const flareAfterDelete = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert.equal(flareAfterDelete.profile, null)
|
|
assert.deepEqual(flareAfterDelete.suggestions, [])
|
|
assert.deepEqual(flareAfterDelete.likes, [])
|
|
assert.deepEqual(flareAfterDelete.matches, [])
|
|
const repeatedFlareDelete = await post(baseUrl, 'flare:delete-profile')
|
|
assert.deepEqual(repeatedFlareDelete, {
|
|
error: 'profile_not_found',
|
|
success: false,
|
|
})
|
|
const rejectedEmptyFlare = await post(baseUrl, 'flare:save-profile', {
|
|
...flareBeforeDelete.profile,
|
|
photoMediaIds: [],
|
|
photoUrls: undefined,
|
|
})
|
|
assert.deepEqual(rejectedEmptyFlare, {
|
|
error: 'invalid_profile_photos',
|
|
success: false,
|
|
})
|
|
const flareStillDeleted = await expectSuccess(
|
|
baseUrl,
|
|
'flare:bootstrap',
|
|
{},
|
|
true,
|
|
)
|
|
assert.equal(
|
|
flareStillDeleted.profile,
|
|
null,
|
|
'an invalid empty Flare profile was persisted',
|
|
)
|
|
const recreatedFlare = await expectSuccess(
|
|
baseUrl,
|
|
'flare:save-profile',
|
|
{
|
|
...flareBeforeDelete.profile,
|
|
photoMediaIds: [retainedProfilePhotoId],
|
|
photoUrls: undefined,
|
|
},
|
|
true,
|
|
)
|
|
assert(
|
|
recreatedFlare.suggestions.some(
|
|
(profile) => profile.id === swipedProfile.id,
|
|
),
|
|
'recreated Flare profile retained a deleted swipe filter',
|
|
)
|
|
assert.deepEqual(
|
|
recreatedFlare.profile.photoMediaIds,
|
|
[retainedProfilePhotoId],
|
|
'recreated Flare profile did not retain its valid gallery photo',
|
|
)
|
|
|
|
await expectSuccess(baseUrl, 'account:logout')
|
|
const signedOutFlareBootstrap = await post(baseUrl, 'flare:bootstrap')
|
|
assert.deepEqual(signedOutFlareBootstrap, {
|
|
error: 'not_authenticated',
|
|
success: false,
|
|
})
|
|
const signedOutFlareDelete = await post(baseUrl, 'flare:delete-profile')
|
|
assert.deepEqual(signedOutFlareDelete, {
|
|
error: 'not_authenticated',
|
|
success: false,
|
|
})
|
|
}
|
|
|
|
async function main() {
|
|
const server = app.listen(0, '127.0.0.1')
|
|
await once(server, 'listening')
|
|
const address = server.address()
|
|
const baseUrl = `http://127.0.0.1:${address.port}`
|
|
|
|
try {
|
|
const dataByEndpoint = new Map()
|
|
for (const [endpoint, body] of browserDataRequests) {
|
|
dataByEndpoint.set(
|
|
endpoint,
|
|
await expectSuccess(baseUrl, endpoint, body, true),
|
|
)
|
|
}
|
|
verifyBrowserTestData(dataByEndpoint)
|
|
|
|
await verifyStatefulActions(baseUrl)
|
|
|
|
const lifecycleEndpoints = [
|
|
'camera:setActive',
|
|
'camera:setLocked',
|
|
'camera:setFacing',
|
|
'camera:setFlash',
|
|
'camera:setFocus',
|
|
'camera:setOrientation',
|
|
'camera:setZoom',
|
|
'close',
|
|
'custom-app:lifecycle',
|
|
'device:notification-open',
|
|
'notification:focus',
|
|
'sim:picker-close',
|
|
'ui:input-focus',
|
|
'ui:opened',
|
|
'ui:ready',
|
|
]
|
|
for (const endpoint of lifecycleEndpoints) {
|
|
await expectSuccess(baseUrl, endpoint)
|
|
}
|
|
|
|
await expectSuccess(baseUrl, 'device:factory-reset')
|
|
const resetBootstrap = await expectSuccess(
|
|
baseUrl,
|
|
'development:bootstrap',
|
|
{ _testScenario: 'setupPreview' },
|
|
true,
|
|
)
|
|
assert.equal(
|
|
resetBootstrap.device.data.settings.payload.settings.setupCompleted,
|
|
false,
|
|
'factory reset did not restore a browser-testable setup state',
|
|
)
|
|
|
|
const unknown = await post(baseUrl, 'development:missing-mock', {})
|
|
assert.deepEqual(unknown, {
|
|
error: 'mock_endpoint_missing',
|
|
success: false,
|
|
})
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()))
|
|
})
|
|
}
|
|
|
|
console.log(
|
|
`Verified ${browserDataRequests.length} browser data endpoints and stateful app actions.`,
|
|
)
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
})
|