fix(skypic): align auth UI and browser mock

This commit is contained in:
Dominik
2026-08-20 09:54:19 +02:00
parent 47c4d8c1d7
commit 14d5304b10
8 changed files with 272 additions and 27 deletions
+5
View File
@@ -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;
+2 -2
View File
@@ -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')
+3
View File
@@ -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')"
+58 -19
View File
@@ -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,
+95
View File
@@ -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,