mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
36b859cabb
* FIX - layer dynamic island above phone camera The phone screen stacking context kept the Dynamic Island below the physical frame and its camera lens. Render the island at device-shell level, position it around the lens, and keep expanded notification spacing in sync. * ADD - preview all dynamic island states Reuse the runtime component for a development-only gallery and size expanded previews to the actual phone display so they cannot be clipped. * FIX - match dynamic island live activity behavior Show each activity only outside its owning app, use reference-sized activity layouts, and remove the separate expand glyph in favor of direct island taps. * FIX - refine dynamic island live controls Keep background activity state separate from foreground visibility so the island remains visible in the compact closed-phone peek. Match the timer, stopwatch, music, collapse, and spacing behavior to the supplied references. * FIX - keep business call availability after closing Stop treating the closed phone UI as the end of business-call readiness. Route background service-line calls only after revalidating the employee, owned device, and registered SIM. * FIX - remove dock apps from home grid The stored Home layout treated dock and grid shortcuts independently, so default dock apps appeared twice. Let the dock own those default placements and migrate saved layouts by removing matching grid shortcuts. * FIX - keep memo island active after closing Memo recording was cancelled as soon as the UI closed, while uploads required an active UI session and reopening a locked phone forced the home route. Keep the recorder bound to the physical device, revalidate device ownership server-side for uploads, preserve the last route behind the lock screen, and render only a compact frame, display, and live Dynamic Island while closed. * ENH - align live activity controls with phone hardware Report compact live-activity state to the client so Space opens Home or the enabled lock screen contextually. Apply the phone hardware volume as a shared output level for app media, games, YouTube playback, radio, and auxiliary sounds, and render the volume speaker icon in grey. * FIX - hide paused music live activity The music island previously depended only on the selected track, which remains set after playback pauses. Require active playback as well so the island closes when music stops while preserving the track for later resume. --------- Co-authored-by: DerEchteAlec <bycraky@gmail.com>
273 lines
11 KiB
TypeScript
273 lines
11 KiB
TypeScript
import { readFileSync } from 'node:fs'
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const readResourceFile = (path: string) =>
|
|
readFileSync(new URL(`../../sky_phone/${path}`, import.meta.url), 'utf8').replace(/\r\n/g, '\n')
|
|
const readFrontendFile = (path: string) =>
|
|
readFileSync(new URL(path, import.meta.url), 'utf8')
|
|
|
|
const inventoryAdapters = [
|
|
['ox', 'source/bridge/server/inventory/ox.lua'],
|
|
['qb', 'source/bridge/server/inventory/qb.lua'],
|
|
['lj', 'source/bridge/server/inventory/qb.lua'],
|
|
['qs', 'source/bridge/server/inventory/qs.lua'],
|
|
['codem', 'source/bridge/server/inventory/codem.lua'],
|
|
['core', 'source/bridge/server/inventory/core.lua'],
|
|
['mf', 'source/bridge/server/inventory/mf.lua'],
|
|
['smx', 'source/bridge/server/inventory/smx.lua'],
|
|
['hex', 'source/bridge/server/inventory/esx.lua'],
|
|
['esx', 'source/bridge/server/inventory/esx.lua'],
|
|
] as const
|
|
|
|
describe('phone inventory contracts', () => {
|
|
it.each(inventoryAdapters)(
|
|
'registers the phone as a usable item through the %s adapter',
|
|
(_inventory, path) => {
|
|
expect(readResourceFile(path)).toContain(
|
|
'function Bridge.Inventory.RegisterUsableItem',
|
|
)
|
|
},
|
|
)
|
|
|
|
it('fails startup when the selected inventory cannot register the phone item', () => {
|
|
const phoneServer = readResourceFile('source/server/phone.lua')
|
|
|
|
expect(phoneServer).toContain(
|
|
'Bridge.Inventory.RegisterUsableItem(Config.Phone.Item, open_phone)',
|
|
)
|
|
expect(phoneServer).toContain('if not usable_registered then')
|
|
})
|
|
|
|
it('auto-detects HEX and limits count-based ESX inventories to metadata-free modes', () => {
|
|
const inventoryBridge = readResourceFile(
|
|
'source/bridge/server/inventory.lua',
|
|
)
|
|
|
|
expect(inventoryBridge).toContain(
|
|
'GetResourceState("hex_4_inventory") == "started"',
|
|
)
|
|
expect(inventoryBridge).toContain('configured_inventory = "hex"')
|
|
expect(inventoryBridge).toContain('Config.Phone.Unique ~= false')
|
|
expect(inventoryBridge).toContain('Config.Sim.Enabled ~= false')
|
|
})
|
|
|
|
it('provides the LB IsOpen export alias from the authoritative client state', () => {
|
|
const phoneClient = readResourceFile('source/client/main.lua')
|
|
const phoneBridge = readResourceFile('source/bridge/phones/client/lb.lua')
|
|
|
|
expect(phoneBridge).toContain(
|
|
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsOpen"',
|
|
)
|
|
expect(phoneClient).toContain('open = is_open')
|
|
expect(phoneBridge).toContain('return get_phone_state_value("open")')
|
|
})
|
|
|
|
it('provides the LB equipped phone number exports from authoritative device state', () => {
|
|
const phoneClient = readResourceFile('source/client/main.lua')
|
|
const phoneServer = readResourceFile('source/server/phone.lua')
|
|
const clientBridge = readResourceFile('source/bridge/phones/client/lb.lua')
|
|
const serverBridge = readResourceFile('source/bridge/phones/server/lb.lua')
|
|
const serverLifecycle = readResourceFile(
|
|
'source/bridge/phones/server/lifecycle.lua',
|
|
)
|
|
|
|
expect(clientBridge).toMatch(
|
|
/"GetEquippedPhoneNumber",\s+phone\.GetEquippedPhoneNumber/,
|
|
)
|
|
expect(phoneClient).toContain('return device_payload.device.sim.number')
|
|
expect(phoneClient).toContain(
|
|
'Bridge.Callbacks.Trigger("sky_phone:device:equipped-number", {})',
|
|
)
|
|
expect(phoneServer).toContain(
|
|
'Bridge.Callbacks.Register("sky_phone:device:equipped-number", function(source)',
|
|
)
|
|
expect(phoneServer).toContain(
|
|
'function SkyPhone.GetEquippedPhoneNumber(player)',
|
|
)
|
|
expect(phoneServer).toContain(
|
|
'cache_equipped_phone_number(source, identifier, device.phone_number)',
|
|
)
|
|
expect(phoneServer).toContain(
|
|
'equipped_phone_sources[phone_number] = source',
|
|
)
|
|
const equippedNumberExport = phoneServer.match(
|
|
/function SkyPhone\.GetEquippedPhoneNumber\(player\)([\s\S]*?)\nfunction SkyPhone\.GetSourceFromNumber/,
|
|
)?.[1]
|
|
const equippedNumberResolver = phoneServer.match(
|
|
/local function resolve_equipped_phone_number\(source\)([\s\S]*?)\nfunction SkyPhone\.GetEquippedPhoneNumber/,
|
|
)?.[1]
|
|
expect(equippedNumberExport).toBeDefined()
|
|
expect(equippedNumberResolver).toBeDefined()
|
|
expect(equippedNumberExport).toContain('type(player) == "number"')
|
|
expect(equippedNumberExport).toContain('online_source_for_identifier(player)')
|
|
expect(phoneServer).toContain('Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)')
|
|
expect(phoneServer).toContain('return resolve_equipped_phone_number(player)')
|
|
expect(equippedNumberExport).not.toContain('tonumber(player)')
|
|
expect(equippedNumberExport).not.toContain('equipped_phone_numbers[player]')
|
|
expect(equippedNumberResolver).not.toContain('return cached_number')
|
|
expect(phoneServer).toContain(
|
|
'player_source and resolve_equipped_phone_number(player_source) == normalized',
|
|
)
|
|
expect(serverBridge).toMatch(
|
|
/"GetEquippedPhoneNumber",\s+phone\.GetEquippedPhoneNumber/,
|
|
)
|
|
expect(serverBridge).toMatch(
|
|
/"GetSourceFromNumber",\s+phone\.GetSourceFromNumber/,
|
|
)
|
|
expect(phoneServer).toContain(
|
|
'TriggerEvent("sky_phone:server:phoneNumberChanged", source, phone_number)',
|
|
)
|
|
expect(serverLifecycle).toContain(
|
|
'SkyPhoneCompatibility.EmitServerProviderStop(LB_PROVIDER_NAME)',
|
|
)
|
|
expect(serverLifecycle).toContain(
|
|
'SkyPhoneCompatibility.EmitServerProviderStart(LB_PROVIDER_NAME)',
|
|
)
|
|
})
|
|
|
|
it('maps LB client lifecycle and state contracts', () => {
|
|
const phoneClient = readResourceFile('source/client/main.lua')
|
|
const phoneBridge = readResourceFile('source/bridge/phones/client/lb.lua')
|
|
|
|
expect(phoneBridge).toContain(
|
|
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "ToggleOpen"',
|
|
)
|
|
expect(phoneClient).toContain(
|
|
'local result = Bridge.Callbacks.Trigger("sky_phone:device:open-request", {})',
|
|
)
|
|
expect(phoneClient).toMatch(
|
|
/result\.success ~= true[\s\S]*open_without_focus = false[\s\S]*return false/,
|
|
)
|
|
expect(phoneBridge).toContain(
|
|
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsPhoneOnScreen"',
|
|
)
|
|
expect(phoneBridge).toContain(
|
|
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "IsInCall"',
|
|
)
|
|
expect(phoneBridge).toContain(
|
|
'SkyPhoneCompatibility.RegisterExportAlias("lb-phone", "FormatNumber", client_bridge.FormatNumber)',
|
|
)
|
|
expect(phoneClient).toContain(
|
|
'TriggerEvent("sky_phone:client:phoneNumberChanged", next_number)',
|
|
)
|
|
expect(phoneClient).toContain(
|
|
'TriggerEvent("sky_phone:client:phoneToggled", true)',
|
|
)
|
|
expect(phoneClient).toContain(
|
|
'TriggerEvent("sky_phone:client:phoneToggled", false)',
|
|
)
|
|
expect(phoneBridge).toContain('TriggerEvent("lb-phone:numberChanged", phone_number)')
|
|
expect(phoneBridge).toContain('TriggerEvent("lb-phone:phoneToggled", open)')
|
|
})
|
|
|
|
it('keeps vendor contracts outside the phone business core', () => {
|
|
const corePaths = [
|
|
'source/client/main.lua',
|
|
'source/client/camera.lua',
|
|
'source/client/custom_apps.lua',
|
|
'source/server/phone.lua',
|
|
'source/server/sim.lua',
|
|
'source/server/media.lua',
|
|
]
|
|
|
|
for (const path of corePaths) {
|
|
expect(readResourceFile(path)).not.toMatch(
|
|
/lb-phone|17mov|high-phone|qs-smartphone|yseries|SkyPhoneCompatibility/,
|
|
)
|
|
}
|
|
})
|
|
|
|
it('maps the LB custom-app delete lifecycle from the App Store to Lua', () => {
|
|
const appStore = readFrontendFile('stores/app-store.ts')
|
|
const customApps = readResourceFile('source/client/custom_apps.lua')
|
|
const compatibility = readResourceFile('source/bridge/phones/shared/lb.lua')
|
|
|
|
expect(appStore).toContain("event: 'delete'")
|
|
expect(customApps).toContain('and lifecycle_event ~= "delete"')
|
|
expect(customApps).toContain(
|
|
'invoke_or_defer_hook(app, "onDelete", lifecycle_payload, deferred_hooks)',
|
|
)
|
|
expect(compatibility).toContain('onDelete = app_data.onDelete')
|
|
})
|
|
|
|
it('emits exact LB observer events after authoritative state changes', () => {
|
|
const phonePersistence = readResourceFile(
|
|
'source/server/phone_persistence.lua',
|
|
)
|
|
const simServer = readResourceFile('source/server/sim.lua')
|
|
const mediaServer = readResourceFile('source/server/media.lua')
|
|
const phoneBridge = readResourceFile(
|
|
'source/bridge/phones/server/lifecycle.lua',
|
|
)
|
|
|
|
expect(simServer).toContain(
|
|
'TriggerEvent("sky_phone:server:phoneNumberGenerated", source, sim.phone_number)',
|
|
)
|
|
expect(phonePersistence).toContain(
|
|
'TriggerEvent("sky_phone:server:factoryReset", source, phone_number)',
|
|
)
|
|
expect(mediaServer).toContain(
|
|
'TriggerEvent("sky_phone:server:galleryMediaDeleted", src, phone_number, deleted_link)',
|
|
)
|
|
expect(phoneBridge).toContain('TriggerEvent("lb-phone:phoneNumberGenerated"')
|
|
expect(phoneBridge).toContain('TriggerEvent("lb-phone:factoryReset"')
|
|
expect(phoneBridge).toContain('TriggerEvent("lb-phone:deletedFromGallery"')
|
|
})
|
|
|
|
it('opens from a configurable F1 mapping without client-provided device identity', () => {
|
|
const config = readResourceFile('config/config.lua')
|
|
const phoneClient = readResourceFile('source/client/main.lua')
|
|
const phoneServer = readResourceFile('source/server/phone.lua')
|
|
|
|
expect(config).toContain('Keybind = "F1"')
|
|
expect(phoneClient).toContain(
|
|
'RegisterKeyMapping("sky_phone_toggle", locale.Controls.OpenPhone, "keyboard", Config.Phone.Keybind)',
|
|
)
|
|
expect(phoneClient).toContain(
|
|
'Bridge.Callbacks.Trigger("sky_phone:device:open-request", {})',
|
|
)
|
|
expect(phoneServer).toContain(
|
|
'Bridge.Callbacks.Register("sky_phone:device:open-request", function(source)',
|
|
)
|
|
})
|
|
|
|
it('opens a running live activity with Space without affecting normal gameplay', () => {
|
|
const phoneClient = readResourceFile('source/client/main.lua')
|
|
|
|
expect(phoneClient).toContain(
|
|
'RegisterCommand("sky_phone_live_activity_open"',
|
|
)
|
|
expect(phoneClient).toContain(
|
|
'if not live_activity_active or is_open or open_requested then',
|
|
)
|
|
expect(phoneClient).toContain(
|
|
'RegisterKeyMapping(\n "sky_phone_live_activity_open"',
|
|
)
|
|
expect(phoneClient).toContain('"SPACE"')
|
|
expect(phoneClient).toContain('RegisterNUICallback("ui:live-activity"')
|
|
})
|
|
|
|
it('keeps a server-selected unique handset as the preferred hotkey device', () => {
|
|
const phoneServer = readResourceFile('source/server/phone.lua')
|
|
|
|
expect(phoneServer).toContain('local preferred_device_imeis = {}')
|
|
expect(phoneServer).toContain(
|
|
'local preferred_imei = preferred_device_imeis[source]',
|
|
)
|
|
expect(phoneServer).toContain('preferred_device_imeis[source] = imei')
|
|
})
|
|
|
|
it('keeps non-unique phones bound to one persistent device per character', () => {
|
|
const phoneServer = readResourceFile('source/server/phone.lua')
|
|
const migration = readResourceFile('source/server/db_migrate.lua')
|
|
|
|
expect(phoneServer).toContain('if not unique_phones then')
|
|
expect(phoneServer).toContain('return map_character_device(source, slot)')
|
|
expect(phoneServer).toContain('FROM `sky_phone_character_devices`')
|
|
expect(phoneServer).toContain('WHERE `owner_identifier` = ?')
|
|
expect(migration).toContain('name = "sky_phone_character_devices"')
|
|
expect(migration).toContain('primaryKey = "owner_identifier"')
|
|
})
|
|
})
|