mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 01:08:59 +00:00
fix(skypic): align auth UI and browser mock
This commit is contained in:
@@ -577,6 +577,11 @@ pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The browser mock links `demo@ifruit.com` and signs in the seeded SkyPic profile
|
||||
`@alexm` by default. To exercise SkyPic registration with an empty profile, open
|
||||
`http://localhost:5174/?testScenario=skypic-onboarding#/apps/skypic` while the
|
||||
development server is running.
|
||||
|
||||
Create a production frontend build with:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import AppProfileAuth from './AppProfileAuth.vue'
|
||||
|
||||
const source = readFileSync(
|
||||
new URL('./AppProfileAuth.vue', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
async function renderAuth(
|
||||
mode: 'login' | 'register',
|
||||
movingModeHighlight = true,
|
||||
): Promise<string> {
|
||||
return renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(AppProfileAuth, {
|
||||
avatarUrl: null,
|
||||
body: 'Use the linked account.',
|
||||
cameraLabel: 'Camera',
|
||||
email: 'demo@ifruit.com',
|
||||
emailLabel: 'SkyPic account',
|
||||
error: '',
|
||||
eyebrow: 'Your SkyPic account',
|
||||
galleryLabel: 'Photos',
|
||||
loginLabel: 'Continue to SkyPic',
|
||||
loginModeLabel: 'Login',
|
||||
mode,
|
||||
movingModeHighlight,
|
||||
pending: false,
|
||||
registerLabel: 'Create profile',
|
||||
registerModeLabel: 'Register',
|
||||
title: 'Welcome back',
|
||||
username: mode === 'login' ? 'alexm' : 'newprofile',
|
||||
usernameLabel: 'Handle',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
describe('AppProfileAuth', () => {
|
||||
it('uses the rounded Sky UI moving highlight for its mode switch', async () => {
|
||||
const html = await renderAuth('login')
|
||||
|
||||
expect(html).toContain('sky-segmented--strong')
|
||||
expect(html).toContain('sky-segmented--rounded')
|
||||
expect(html).toContain('sky-segmented__highlight')
|
||||
expect(html).toContain('app-profile-auth__mode--moving-highlight')
|
||||
expect(html).toContain('width:calc(50% - 4px)')
|
||||
expect(html).toContain('--sky-segmented-indicator-offset:calc(0% + 0px)')
|
||||
expect(html).toContain('aria-label="Your SkyPic account"')
|
||||
})
|
||||
|
||||
it('moves the highlight and keeps short mode labels separate from actions', async () => {
|
||||
const html = await renderAuth('register')
|
||||
|
||||
expect(html).toContain('--sky-segmented-indicator-offset:calc(100% + 4px)')
|
||||
expect(html.match(/Login/g)).toHaveLength(1)
|
||||
expect(html.match(/Register/g)).toHaveLength(1)
|
||||
expect(html).toContain('Create profile')
|
||||
expect(html).not.toContain('>Continue to SkyPic</button>')
|
||||
})
|
||||
|
||||
it('keeps the moving highlight opt-in for SkyPic', async () => {
|
||||
const html = await renderAuth('login', false)
|
||||
|
||||
expect(html).not.toContain('sky-segmented--strong')
|
||||
expect(html).not.toContain('sky-segmented__highlight')
|
||||
expect(html).toContain('app-profile-auth__mode-button--login')
|
||||
expect(html).toContain('app-profile-auth__mode-button--active')
|
||||
expect(source).toContain(':strong="movingModeHighlight"')
|
||||
expect(source).toContain(
|
||||
':not(.app-profile-auth__mode--moving-highlight)',
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -30,14 +30,17 @@ const props = withDefaults(
|
||||
eyebrow: string
|
||||
galleryLabel: string
|
||||
loginLabel: string
|
||||
loginModeLabel?: string
|
||||
maxUsernameLength?: number
|
||||
minUsernameLength?: number
|
||||
mode: 'login' | 'register'
|
||||
movingModeHighlight?: boolean
|
||||
password?: string
|
||||
passwordLabel?: string
|
||||
passwordPlaceholder?: string
|
||||
pending: boolean
|
||||
registerLabel: string
|
||||
registerModeLabel?: string
|
||||
requirePassword?: boolean
|
||||
submitEnabled?: boolean
|
||||
title: string
|
||||
@@ -62,10 +65,13 @@ const props = withDefaults(
|
||||
emailAsField: false,
|
||||
maxUsernameLength: 40,
|
||||
minUsernameLength: 2,
|
||||
loginModeLabel: '',
|
||||
movingModeHighlight: false,
|
||||
password: '',
|
||||
passwordLabel: 'Password',
|
||||
passwordPlaceholder: '',
|
||||
requirePassword: false,
|
||||
registerModeLabel: '',
|
||||
showConfirmPassword: false,
|
||||
submitEnabled: undefined,
|
||||
usernameAutocomplete: 'username',
|
||||
@@ -101,6 +107,10 @@ const canSubmit = computed(() => {
|
||||
)
|
||||
)
|
||||
})
|
||||
const modeLoginLabel = computed(() => props.loginModeLabel || props.loginLabel)
|
||||
const modeRegisterLabel = computed(
|
||||
() => props.registerModeLabel || props.registerLabel,
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -119,9 +129,15 @@ const canSubmit = computed(() => {
|
||||
|
||||
<SkyGlass class="app-profile-auth__card">
|
||||
<SkySegmented
|
||||
:active-index="movingModeHighlight ? (mode === 'login' ? 0 : 1) : undefined"
|
||||
:aria-label="eyebrow"
|
||||
:item-count="movingModeHighlight ? 2 : undefined"
|
||||
raised
|
||||
:rounded="movingModeHighlight"
|
||||
:strong="movingModeHighlight"
|
||||
class="app-profile-auth__mode"
|
||||
:class="{
|
||||
'app-profile-auth__mode--moving-highlight': movingModeHighlight,
|
||||
'app-profile-auth__mode--register': mode === 'register',
|
||||
}"
|
||||
>
|
||||
@@ -133,7 +149,7 @@ const canSubmit = computed(() => {
|
||||
:active="mode === 'login'"
|
||||
@click="emit('update:mode', 'login')"
|
||||
>
|
||||
{{ loginLabel }}
|
||||
{{ modeLoginLabel }}
|
||||
</SkySegmentedButton>
|
||||
<SkySegmentedButton
|
||||
class="app-profile-auth__mode-button app-profile-auth__mode-button--register"
|
||||
@@ -143,7 +159,7 @@ const canSubmit = computed(() => {
|
||||
:active="mode === 'register'"
|
||||
@click="emit('update:mode', 'register')"
|
||||
>
|
||||
{{ registerLabel }}
|
||||
{{ modeRegisterLabel }}
|
||||
</SkySegmentedButton>
|
||||
</SkySegmented>
|
||||
|
||||
@@ -372,21 +388,24 @@ const canSubmit = computed(() => {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight) {
|
||||
padding: 3px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 14px;
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
.app-profile-auth__mode :deep(.app-profile-auth__mode-button) {
|
||||
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight)
|
||||
:deep(.app-profile-auth__mode-button) {
|
||||
border-radius: 3px;
|
||||
}
|
||||
.app-profile-auth__mode
|
||||
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight)
|
||||
:deep(
|
||||
.app-profile-auth__mode-button--login.app-profile-auth__mode-button--active
|
||||
) {
|
||||
border-radius: 10px 3px 3px 10px;
|
||||
}
|
||||
.app-profile-auth__mode
|
||||
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight)
|
||||
:deep(
|
||||
.app-profile-auth__mode-button--register.app-profile-auth__mode-button--active
|
||||
) {
|
||||
@@ -685,7 +704,8 @@ const canSubmit = computed(() => {
|
||||
padding: 14px;
|
||||
border-radius: 28px;
|
||||
}
|
||||
.app-profile-auth--centered .app-profile-auth__mode {
|
||||
.app-profile-auth--centered
|
||||
.app-profile-auth__mode:not(.app-profile-auth__mode--moving-highlight) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
|
||||
@@ -389,8 +389,8 @@ describe('SkyPic backend contracts', () => {
|
||||
deletion.indexOf('delete_remote_file(row.remote_id)'),
|
||||
)
|
||||
expect(mediaUtils).toContain("'media_in_use'")
|
||||
expect(fallbackLocales).toContain(
|
||||
"media_in_use:\n 'This media is still used by SkyPic and cannot be deleted yet.'",
|
||||
expect(fallbackLocales).toMatch(
|
||||
/media_in_use:\s*'This media is still used by SkyPic and cannot be deleted yet\.'/,
|
||||
)
|
||||
expect(englishLocale).toContain(
|
||||
'media_in_use = "This media is still used by SkyPic and cannot be deleted yet."',
|
||||
|
||||
@@ -89,6 +89,9 @@ describe('SkyPic frontend contract', () => {
|
||||
expect(viewSource).toContain("appAuth.signIn('skypic', account.email)")
|
||||
expect(viewSource).toContain("appAuth.signOut('skypic')")
|
||||
expect(viewSource).toContain('<AppProfileAuth')
|
||||
expect(viewSource).toContain('moving-mode-highlight')
|
||||
expect(viewSource).toContain(':login-mode-label=')
|
||||
expect(viewSource).toContain(':register-mode-label=')
|
||||
expect(viewSource).toContain('<AccountLogoutDialog')
|
||||
expect(viewSource).toContain('app-id="skypic"')
|
||||
expect(viewSource).toContain('deleteSkyPicAccount')
|
||||
|
||||
@@ -1530,11 +1530,14 @@ onBeforeUnmount(() => {
|
||||
:eyebrow="t('auth.eyebrow')"
|
||||
:gallery-label="t('camera.gallery')"
|
||||
:login-label="t('auth.login')"
|
||||
:login-mode-label="phone.t('Common.appAuth.login')"
|
||||
:max-username-length="24"
|
||||
:min-username-length="3"
|
||||
:mode="authMode"
|
||||
moving-mode-highlight
|
||||
:pending="authSubmitting"
|
||||
:register-label="t('onboarding.create')"
|
||||
:register-mode-label="phone.t('Common.appAuth.register')"
|
||||
:submit-enabled="authSubmitEnabled"
|
||||
:title="t(authMode === 'login' ? 'auth.title' : 'onboarding.title')"
|
||||
:username-label="t('onboarding.handle')"
|
||||
|
||||
@@ -2955,7 +2955,7 @@ const deviceData = {
|
||||
appAuth: {
|
||||
payload: {
|
||||
accountEmail: 'demo@ifruit.com',
|
||||
signedIn: ['citymarkt', 'local-pages', 'feather', 'crewlink'],
|
||||
signedIn: ['citymarkt', 'local-pages', 'feather', 'crewlink', 'skypic'],
|
||||
version: 1,
|
||||
},
|
||||
revision: 1,
|
||||
@@ -8103,7 +8103,11 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skypic:update-profile') {
|
||||
if (!skyPicProfile) {
|
||||
const onboardingScenario = testScenario === 'skypic-onboarding'
|
||||
const currentProfile = onboardingScenario
|
||||
? skyPicOnboardingProfile
|
||||
: skyPicProfile
|
||||
if (!currentProfile) {
|
||||
response.json({ success: false, error: 'profile_required' })
|
||||
return
|
||||
}
|
||||
@@ -8126,7 +8130,7 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
}
|
||||
const avatarSeed = skyPicAvatarSeed(
|
||||
request.body.avatarSeed,
|
||||
skyPicProfile.avatarSeed,
|
||||
currentProfile.avatarSeed,
|
||||
)
|
||||
if (avatarSeed === null) {
|
||||
response.json({ success: false, error: 'invalid_avatar_seed' })
|
||||
@@ -8142,22 +8146,22 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
: mockMedia.find(
|
||||
(item) => item.id === avatarMediaId && item.mediaType === 'photo',
|
||||
)
|
||||
Object.assign(skyPicProfiles[0], {
|
||||
const summary = {
|
||||
avatarSeed,
|
||||
avatarUrl:
|
||||
request.body.avatarMediaId === undefined
|
||||
? skyPicProfile.avatarUrl
|
||||
? currentProfile.avatarUrl
|
||||
: (avatar?.url ?? null),
|
||||
displayName,
|
||||
handle,
|
||||
})
|
||||
skyPicProfile = {
|
||||
...skyPicProfile,
|
||||
...skyPicProfiles[0],
|
||||
}
|
||||
const updatedProfile = {
|
||||
...currentProfile,
|
||||
...summary,
|
||||
allowStoryReplies: request.body.allowStoryReplies === true,
|
||||
avatarMediaId:
|
||||
request.body.avatarMediaId === undefined
|
||||
? skyPicProfile.avatarMediaId
|
||||
? currentProfile.avatarMediaId
|
||||
: (avatar?.id ?? null),
|
||||
bio: String(request.body.bio ?? '')
|
||||
.trim()
|
||||
@@ -8165,7 +8169,18 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
showInQuickAdd: request.body.showInQuickAdd === true,
|
||||
storyPrivacy,
|
||||
}
|
||||
response.json({ success: true, data: { ...skyPicProfile } })
|
||||
if (onboardingScenario) {
|
||||
skyPicOnboardingProfile = updatedProfile
|
||||
} else {
|
||||
Object.assign(skyPicProfiles[0], summary)
|
||||
skyPicProfile = { ...updatedProfile, ...skyPicProfiles[0] }
|
||||
}
|
||||
response.json({
|
||||
success: true,
|
||||
data: {
|
||||
...(onboardingScenario ? skyPicOnboardingProfile : skyPicProfile),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
if (endpoint === 'skypic:search') {
|
||||
@@ -10931,6 +10946,9 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
return
|
||||
}
|
||||
if (endpoint === 'development:bootstrap') {
|
||||
if (testScenario === 'skypic-onboarding') {
|
||||
skyPicOnboardingProfile = null
|
||||
}
|
||||
cryptoRegistered = testScenario !== 'crypto-register'
|
||||
cryptoAuthenticated = !['crypto-login', 'crypto-register'].includes(
|
||||
testScenario,
|
||||
@@ -11067,7 +11085,8 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
testScenario.startsWith('citymarkt-') ||
|
||||
testScenario.startsWith('feather-') ||
|
||||
testScenario.startsWith('local-pages-') ||
|
||||
testScenario.startsWith('crewlink-')
|
||||
testScenario.startsWith('crewlink-') ||
|
||||
testScenario.startsWith('skypic-')
|
||||
? {
|
||||
...deviceData,
|
||||
apps: {
|
||||
@@ -11078,9 +11097,11 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
dock: [],
|
||||
grid: testScenario.startsWith('crewlink-')
|
||||
? ['crewlink']
|
||||
: testScenario === 'citymarkt-local-pages-missing'
|
||||
? ['citymarkt']
|
||||
: ['citymarkt', 'local-pages'],
|
||||
: testScenario.startsWith('skypic-')
|
||||
? ['skypic']
|
||||
: testScenario === 'citymarkt-local-pages-missing'
|
||||
? ['citymarkt']
|
||||
: ['citymarkt', 'local-pages'],
|
||||
hidden:
|
||||
testScenario === 'citymarkt-local-pages-missing'
|
||||
? ['local-pages']
|
||||
@@ -11098,17 +11119,35 @@ app.post('/api/:endpoint', (request, response) => {
|
||||
'local-pages-register',
|
||||
'crewlink-login',
|
||||
'crewlink-register',
|
||||
'skypic-onboarding',
|
||||
].includes(testScenario)
|
||||
? {
|
||||
payload: {
|
||||
accountEmail: linkedAccount?.email ?? '',
|
||||
signedIn: testScenario.startsWith('feather-')
|
||||
? ['citymarkt', 'local-pages', 'crewlink']
|
||||
? ['citymarkt', 'local-pages', 'crewlink', 'skypic']
|
||||
: testScenario.startsWith('local-pages-')
|
||||
? ['citymarkt', 'feather', 'crewlink']
|
||||
? ['citymarkt', 'feather', 'crewlink', 'skypic']
|
||||
: testScenario.startsWith('crewlink-')
|
||||
? ['citymarkt', 'local-pages', 'feather']
|
||||
: ['local-pages', 'feather', 'crewlink'],
|
||||
? [
|
||||
'citymarkt',
|
||||
'local-pages',
|
||||
'feather',
|
||||
'skypic',
|
||||
]
|
||||
: testScenario.startsWith('skypic-')
|
||||
? [
|
||||
'citymarkt',
|
||||
'local-pages',
|
||||
'feather',
|
||||
'crewlink',
|
||||
]
|
||||
: [
|
||||
'local-pages',
|
||||
'feather',
|
||||
'crewlink',
|
||||
'skypic',
|
||||
],
|
||||
version: 1,
|
||||
},
|
||||
revision: deviceData.appAuth?.revision ?? 0,
|
||||
|
||||
@@ -123,6 +123,10 @@ function expectSkyPicMetadataSafe(items, label) {
|
||||
|
||||
function verifyBrowserTestData(dataByEndpoint) {
|
||||
const development = dataByEndpoint.get('development:bootstrap')
|
||||
assert(
|
||||
development.device.data.appAuth.payload.signedIn.includes('skypic'),
|
||||
'default browser demo did not sign in the seeded SkyPic profile',
|
||||
)
|
||||
expectItems(development.device.data.alarms.payload, 'clock alarms', 3)
|
||||
expectItems(
|
||||
development.device.data.media.payload.captures,
|
||||
@@ -853,6 +857,22 @@ async function verifySkyPicActions(baseUrl) {
|
||||
success: false,
|
||||
},
|
||||
)
|
||||
const onboardingDevelopment = await expectSuccess(
|
||||
baseUrl,
|
||||
'development:bootstrap',
|
||||
{ _testScenario: 'skypic-onboarding' },
|
||||
true,
|
||||
)
|
||||
assert.deepEqual(
|
||||
onboardingDevelopment.device.data.apps.payload.homeLayout.grid,
|
||||
['skypic'],
|
||||
)
|
||||
assert(
|
||||
!onboardingDevelopment.device.data.appAuth.payload.signedIn.includes(
|
||||
'skypic',
|
||||
),
|
||||
'SkyPic onboarding browser demo started with an existing app session',
|
||||
)
|
||||
const onboarding = await expectSuccess(
|
||||
baseUrl,
|
||||
'skypic:bootstrap',
|
||||
@@ -878,6 +898,81 @@ async function verifySkyPicActions(baseUrl) {
|
||||
)
|
||||
assert.equal(createdOnboarding.profile.handle, 'alexm')
|
||||
assert.equal(createdOnboarding.profile.snapScore, 0)
|
||||
const onboardingProfileUpdate = {
|
||||
_testScenario: 'skypic-onboarding',
|
||||
allowStoryReplies: false,
|
||||
avatarMediaId: null,
|
||||
avatarSeed: 144,
|
||||
bio: 'Updated inside the isolated SkyPic onboarding preview.',
|
||||
displayName: 'Onboarding Alex',
|
||||
handle: 'onboarding.alex',
|
||||
showInQuickAdd: false,
|
||||
storyPrivacy: 'everyone',
|
||||
}
|
||||
const updatedOnboardingProfile = await expectSuccess(
|
||||
baseUrl,
|
||||
'skypic:update-profile',
|
||||
onboardingProfileUpdate,
|
||||
true,
|
||||
)
|
||||
assert.equal(updatedOnboardingProfile.handle, 'onboarding.alex')
|
||||
assert.equal(updatedOnboardingProfile.displayName, 'Onboarding Alex')
|
||||
assert.equal(updatedOnboardingProfile.avatarSeed, 144)
|
||||
assert.equal(updatedOnboardingProfile.avatarMediaId, null)
|
||||
assert.equal(updatedOnboardingProfile.avatarUrl, null)
|
||||
assert.equal(updatedOnboardingProfile.snapScore, 0)
|
||||
const updatedOnboarding = await expectSuccess(
|
||||
baseUrl,
|
||||
'skypic:bootstrap',
|
||||
{ _testScenario: 'skypic-onboarding' },
|
||||
true,
|
||||
)
|
||||
assert.equal(updatedOnboarding.profile.handle, 'onboarding.alex')
|
||||
assert.equal(updatedOnboarding.profile.bio, onboardingProfileUpdate.bio)
|
||||
const defaultProfileAfterOnboardingUpdate = await expectSuccess(
|
||||
baseUrl,
|
||||
'skypic:bootstrap',
|
||||
{},
|
||||
true,
|
||||
)
|
||||
assert.equal(
|
||||
defaultProfileAfterOnboardingUpdate.profile.handle,
|
||||
profile.handle,
|
||||
)
|
||||
assert.equal(
|
||||
defaultProfileAfterOnboardingUpdate.profile.displayName,
|
||||
profile.displayName,
|
||||
)
|
||||
|
||||
const reloadedOnboardingDevelopment = await expectSuccess(
|
||||
baseUrl,
|
||||
'development:bootstrap',
|
||||
{ _testScenario: 'skypic-onboarding' },
|
||||
true,
|
||||
)
|
||||
assert(
|
||||
!reloadedOnboardingDevelopment.device.data.appAuth.payload.signedIn.includes(
|
||||
'skypic',
|
||||
),
|
||||
'reloaded SkyPic onboarding browser demo restored an app session',
|
||||
)
|
||||
const reloadedOnboarding = await expectSuccess(
|
||||
baseUrl,
|
||||
'skypic:bootstrap',
|
||||
{ _testScenario: 'skypic-onboarding' },
|
||||
true,
|
||||
)
|
||||
assert.equal(reloadedOnboarding.profile, null)
|
||||
const recreatedAfterReload = await expectSuccess(
|
||||
baseUrl,
|
||||
'skypic:create-profile',
|
||||
{
|
||||
...createProfile,
|
||||
_testScenario: 'skypic-onboarding',
|
||||
},
|
||||
true,
|
||||
)
|
||||
assert.equal(recreatedAfterReload.handle, 'alexm')
|
||||
assert.deepEqual(
|
||||
await post(baseUrl, 'skypic:create-profile', {
|
||||
...createProfile,
|
||||
|
||||
Reference in New Issue
Block a user