diff --git a/samples/browser/quickstart/src/components/profileDialog.js b/samples/browser/quickstart/src/components/profileDialog.js
index 1f0634b2..36e30dd8 100644
--- a/samples/browser/quickstart/src/components/profileDialog.js
+++ b/samples/browser/quickstart/src/components/profileDialog.js
@@ -1,6 +1,20 @@
-import { updateMeProfile } from '@thunderid/browser'
+import { deepMerge, getUsersMe, getUsersMeMeta, updateMeProfile } from '@thunderid/browser'
const ICON_CLOSE = ``
+const ICON_PENCIL = ``
+
+// Attributes that are always read-only regardless of schema mutability
+const ALWAYS_READONLY_KEYS = [
+ 'attributes',
+ 'id',
+ 'isReadOnly',
+ 'isReadonly',
+ 'ouId',
+ 'sub',
+ 'username',
+ 'userName',
+ 'user_name',
+]
function escapeHtml(str) {
if (str == null) return ''
@@ -12,10 +26,45 @@ function escapeHtml(str) {
.replace(/'/g, ''')
}
+function formatLabel(key) {
+ return key
+ .split(/(?=[A-Z])|[_.]/)
+ .filter(Boolean)
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(' ')
+}
+
+function fieldLabel(key, schemaEntry) {
+ return schemaEntry?.displayName || formatLabel(key)
+}
+
+function isFieldEditable(key, schemaEntry) {
+ if (ALWAYS_READONLY_KEYS.includes(key)) return false
+ if (!schemaEntry) return false
+ if (schemaEntry.credential) return false
+ if (schemaEntry.readOnly || schemaEntry.mutability === 'READ_ONLY') return false
+ return true
+}
+
function getAvatarUrl(user) {
return user?.profile || user?.profileUrl || user?.picture || user?.URL || null
}
+// Deterministic gradient background derived from a name, matching `@thunderid/react`'s
+// `Avatar` `background="random"` (default) behavior โ same hash and HSL formula, so a
+// given user gets the same avatar color here as they would in the React/Vue quickstarts.
+function generateAvatarBackground(name) {
+ const hash = name.split('').reduce((acc, char) => ((acc << 5) - acc + char.charCodeAt(0)) & 0xffffffff, 0)
+ const seed = Math.abs(hash)
+ const hue1 = seed % 360
+ const hue2 = (hue1 + 60 + (seed % 120)) % 360
+ const saturation = 70 + (seed % 20)
+ const lightness1 = 55 + (seed % 15)
+ const lightness2 = 60 + (seed % 15)
+ const angle = 45 + (seed % 91)
+ return `linear-gradient(${angle}deg, hsl(${hue1}, ${saturation}%, ${lightness1}%), hsl(${hue2}, ${saturation}%, ${lightness2}%))`
+}
+
function getInitials(user) {
const given = user?.given_name || ''
const family = user?.family_name || ''
@@ -26,87 +75,239 @@ function getInitials(user) {
return name.slice(0, 2).toUpperCase()
}
-export function renderProfileDialog(user) {
+// `getDisplayName`: first + last name, falling back to
+// username, email, then the `name` attribute.
+function getDisplayName(user, attributes) {
+ const given = attributes?.given_name || user?.given_name
+ const family = attributes?.family_name || user?.family_name
+ if (given && family) return `${given} ${family}`
+ return user?.username || user?.email || attributes?.name || 'User'
+}
+
+function renderAvatarInner(user, displayName) {
const avatarUrl = getAvatarUrl(user)
- const avatarHtml = avatarUrl
- ? `
`
- : escapeHtml(getInitials(user))
- const given = escapeHtml(user?.given_name || '')
- const family = escapeHtml(user?.family_name || '')
+ if (avatarUrl) {
+ return { className: '', html: `
` }
+ }
+ return {
+ className: 'has-gradient',
+ html: escapeHtml(getInitials(user)),
+ style: `background:${generateAvatarBackground(displayName || 'User')}`,
+ }
+}
+
+function createFetcher(auth) {
+ return async (url, config) => {
+ const token = await auth.getAccessToken()
+ return fetch(url, {
+ ...config,
+ headers: { ...config.headers, Authorization: `Bearer ${token}` },
+ })
+ }
+}
+
+// Fetches the schema and the current attribute values needed to render and validate the profile view.
+export async function fetchProfileFormContext({ baseUrl, auth }) {
+ const fetcher = createFetcher(auth)
+
+ const [metaRes, profile] = await Promise.all([
+ getUsersMeMeta({ baseUrl, fetcher }).catch(() => ({ schema: {} })),
+ getUsersMe({ baseUrl, fetcher }).catch(() => null),
+ ])
+
+ return { schema: metaRes?.schema || {}, profile }
+}
+
+function renderFieldRow(key, schemaEntry, value) {
+ if (schemaEntry?.credential) return ''
+
+ const label = fieldLabel(key, schemaEntry)
+ const editable = isFieldEditable(key, schemaEntry)
+ const hasValue = value !== undefined && value !== null && value !== ''
+
+ // BaseUserProfile `shouldShow`: an empty read-only field is hidden entirely rather than rendered as a dash.
+ if (!hasValue && !editable) return ''
+
+ const displayValue = hasValue ? escapeHtml(String(value)) : `Enter your ${label.toLowerCase()}`
+
+ return `
+
-
${avatarHtml}
-
-
-
-
-
-
-
-
-
-
-
-
${email || 'โ'}
-
-
-
-
+
+
+
${avatar.html}
+
+
${escapeHtml(displayName)}
+ ${email ? `
${email}
` : ''}
+
+
+
+
${rows}
`
}
-export function attachProfileDialogHandlers({ user, auth, onSaved }) {
+// Validates a field value against its schema entry (required + regex), matching
+// BaseUserProfile `handleFieldSave`. Returns an error message, or
+// `null` when valid.
+function validateField(schemaEntry, label, value) {
+ if (!schemaEntry) return null
+
+ if (schemaEntry.required && !value) {
+ return `${label} is required.`
+ }
+
+ if (schemaEntry.regex && value) {
+ try {
+ if (!new RegExp(schemaEntry.regex).test(value)) {
+ return `${label} is not in a valid format.`
+ }
+ } catch {
+ // Invalid regex on the schema itself โ nothing to enforce client-side.
+ }
+ }
+
+ return null
+}
+
+export function attachProfileDialogHandlers({ user, auth, schema = {}, profile, onSaved, onClose }) {
const overlay = document.getElementById('profile-dialog-overlay')
- const closeDialog = () => overlay?.remove()
+ const closeDialog = () => {
+ overlay?.remove()
+ onClose?.()
+ }
document.getElementById('profile-dialog-close')?.addEventListener('click', closeDialog)
- document.getElementById('profile-dialog-cancel')?.addEventListener('click', closeDialog)
overlay?.addEventListener('click', (e) => {
if (e.target === overlay) closeDialog()
})
const errorEl = document.getElementById('profile-dialog-error')
- const saveBtn = document.getElementById('profile-dialog-save')
+ const fieldList = document.getElementById('profile-field-list')
+ const baseUrl = import.meta.env.VITE_THUNDERID_BASE_URL
+ const fetcher = createFetcher(auth)
- saveBtn?.addEventListener('click', async () => {
- const givenName = document.getElementById('profile-first-name')?.value.trim() || ''
- const familyName = document.getElementById('profile-last-name')?.value.trim() || ''
+ // Tracks the latest known attributes so successive per-field edits merge against
+ // up-to-date values without refetching the whole profile on every save.
+ let currentAttributes = { ...(profile?.attributes || {}) }
- saveBtn.disabled = true
- saveBtn.textContent = 'Saving...'
- if (errorEl) { errorEl.hidden = true; errorEl.textContent = '' }
+ const refreshHeader = () => {
+ const mergedUser = { ...user, ...currentAttributes }
+ const displayName = getDisplayName(mergedUser, currentAttributes)
- try {
- await updateMeProfile({
- baseUrl: import.meta.env.VITE_THUNDERID_BASE_URL,
- payload: { name: { givenName, familyName } },
- fetcher: async (url, config) => {
- const token = await auth.getAccessToken()
- return fetch(url, {
- ...config,
- headers: { ...config.headers, Authorization: `Bearer ${token}` },
- })
- },
- })
-
- onSaved?.({ ...user, given_name: givenName, family_name: familyName })
- closeDialog()
- } catch (err) {
- if (errorEl) {
- errorEl.hidden = false
- errorEl.textContent = err?.message || 'Failed to update profile. Please try again.'
+ const avatarEl = overlay?.querySelector('.profile-dialog-avatar')
+ if (avatarEl) {
+ const avatar = renderAvatarInner(mergedUser, displayName)
+ avatarEl.className = `profile-dialog-avatar ${avatar.className}`
+ avatarEl.setAttribute('style', avatar.style || '')
+ avatarEl.innerHTML = avatar.html
+ }
+
+ const nameEl = overlay?.querySelector('.profile-dialog-name')
+ if (nameEl) nameEl.textContent = displayName
+ }
+
+ const showError = (message) => {
+ if (!errorEl) return
+ errorEl.hidden = false
+ errorEl.textContent = message
+ }
+ const clearError = () => {
+ if (!errorEl) return
+ errorEl.hidden = true
+ errorEl.textContent = ''
+ }
+
+ const startEdit = (row, key, schemaEntry) => {
+ const display = row.querySelector('.profile-field-row-display')
+ if (!display) return
+ const currentValue = currentAttributes[key] ?? ''
+
+ display.innerHTML = `
+
+
+
+
+
`
+
+ const input = display.querySelector('input')
+ input?.focus()
+
+ const cancel = () => {
+ row.outerHTML = renderFieldRow(key, schemaEntry, currentAttributes[key])
+ }
+
+ const save = async () => {
+ const value = input?.value.trim() || ''
+ const label = fieldLabel(key, schemaEntry)
+ const fieldError = validateField(schemaEntry, label, value)
+ if (fieldError) {
+ showError(fieldError)
+ return
+ }
+ clearError()
+
+ const saveBtn = display.querySelector('[data-action="save"]')
+ if (saveBtn) saveBtn.disabled = true
+
+ try {
+ const mergedAttributes = deepMerge(currentAttributes, { [key]: value })
+ const updatedUser = await updateMeProfile({ baseUrl, payload: mergedAttributes, fetcher })
+
+ currentAttributes = { ...currentAttributes, [key]: updatedUser?.[key] ?? value }
+ row.outerHTML = renderFieldRow(key, schemaEntry, currentAttributes[key])
+ refreshHeader()
+
+ onSaved?.({ ...user, ...currentAttributes })
+ } catch (err) {
+ showError(err?.message || 'Failed to update profile. Please try again.')
+ if (saveBtn) saveBtn.disabled = false
}
- saveBtn.disabled = false
- saveBtn.textContent = 'Save'
}
+
+ display.addEventListener('click', (e) => {
+ const action = e.target.closest('[data-action]')?.dataset.action
+ if (action === 'save') save()
+ if (action === 'cancel') cancel()
+ })
+
+ input?.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') save()
+ if (e.key === 'Escape') cancel()
+ })
+ }
+
+ fieldList?.addEventListener('click', (e) => {
+ const editBtn = e.target.closest('[data-action="edit"]')
+ if (!editBtn) return
+ const row = editBtn.closest('.profile-field-row')
+ const key = row?.dataset.field
+ if (!row || !key) return
+ startEdit(row, key, schema[key])
})
}
diff --git a/samples/browser/quickstart/src/main.js b/samples/browser/quickstart/src/main.js
index f9822a4f..52edd900 100644
--- a/samples/browser/quickstart/src/main.js
+++ b/samples/browser/quickstart/src/main.js
@@ -1,7 +1,7 @@
import './style.css'
import auth, { missingEnvVars } from './auth.js'
import { renderSignedOutNav, renderSignedInNav, attachNavHandlers, attachSignedOutNavHandlers } from './components/nav.js'
-import { renderProfileDialog, attachProfileDialogHandlers } from './components/profileDialog.js'
+import { renderProfileDialog, attachProfileDialogHandlers, fetchProfileFormContext } from './components/profileDialog.js'
import { renderSignedOut, renderHome, renderConfigNeeded, startCountdown, attachSignedOutHandlers, attachConfigNeededHandlers } from './pages/home.js'
import { renderTokenDebug, attachTokenHandlers } from './pages/token.js'
@@ -46,16 +46,25 @@ function renderSignedInPage() {
}
}
-function openManageProfile() {
+async function openManageProfile() {
const app = document.getElementById('app')
if (!app) return
- app.insertAdjacentHTML('beforeend', renderProfileDialog(user))
+ const { schema, profile } = await fetchProfileFormContext({
+ baseUrl: import.meta.env.VITE_THUNDERID_BASE_URL,
+ auth,
+ })
+
+ app.insertAdjacentHTML('beforeend', renderProfileDialog(user, { schema, profile }))
attachProfileDialogHandlers({
user,
auth,
+ schema,
+ profile,
onSaved: (updatedUser) => {
user = updatedUser
+ },
+ onClose: () => {
renderSignedInPage()
},
})
diff --git a/samples/browser/quickstart/src/style.css b/samples/browser/quickstart/src/style.css
index aac59665..27a3de04 100644
--- a/samples/browser/quickstart/src/style.css
+++ b/samples/browser/quickstart/src/style.css
@@ -13,6 +13,7 @@
--blue: #3688FF;
--blue-hover: #2270e8;
--blue-subtle: rgba(54, 136, 255, 0.08);
+ --secondary: #424242;
--shadow-sm: 0 1px 3px rgba(5, 33, 63, 0.08);
--shadow-md: 0 4px 16px rgba(5, 33, 63, 0.10);
--radius: 10px;
@@ -26,6 +27,7 @@
--text: #E0EAFF;
--muted: rgba(224, 234, 255, 0.48);
--blue-subtle: rgba(54, 136, 255, 0.14);
+ --secondary: #8b8b8b;
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
}
@@ -1056,7 +1058,7 @@ body {
.profile-dialog-overlay {
position: fixed;
inset: 0;
- background: rgba(5, 33, 63, 0.45);
+ background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
@@ -1066,24 +1068,25 @@ body {
.profile-dialog {
width: 100%;
- max-width: 380px;
+ max-width: 520px;
+ max-height: 90vh;
+ overflow-y: auto;
background: var(--card);
- border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
- padding: 24px;
}
.profile-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
- margin-bottom: 16px;
+ padding: 24px 32px;
+ border-bottom: 1px solid var(--border);
}
.profile-dialog-header h2 {
- font-size: 16px;
- font-weight: 700;
+ font-size: 19px;
+ font-weight: 600;
color: var(--text);
}
@@ -1105,21 +1108,39 @@ body {
color: var(--blue);
}
+.profile-dialog-body {
+ padding: 16px 32px 32px;
+}
+
+.profile-dialog-summary {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
.profile-dialog-avatar {
- width: 64px;
- height: 64px;
- margin: 0 auto 20px;
+ width: 70px;
+ height: 70px;
border-radius: 50%;
- background: var(--blue);
- color: #fff;
+ border: 1px solid var(--border);
+ background: var(--card);
+ color: var(--text);
display: flex;
align-items: center;
justify-content: center;
- font-size: 20px;
- font-weight: 700;
+ font-size: 28px;
+ font-weight: 600;
overflow: hidden;
}
+.profile-dialog-avatar.has-gradient {
+ color: #fff;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ border: none;
+}
+
.profile-dialog-avatar img {
width: 100%;
height: 100%;
@@ -1136,48 +1157,137 @@ body {
margin-bottom: 14px;
}
-.profile-dialog-field {
- margin-bottom: 14px;
+.profile-dialog-name {
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--text);
}
-.profile-dialog-field label {
- display: block;
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
+.profile-dialog-subtitle {
+ font-size: 14px;
color: var(--muted);
- margin-bottom: 6px;
}
-.profile-dialog-field input {
- width: 100%;
- padding: 9px 12px;
+.profile-field-list {
+ display: flex;
+ flex-direction: column;
+}
+
+.profile-field-row {
+ display: flex;
+ align-items: center;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--border);
+}
+
+.profile-field-row-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--muted);
+ width: 120px;
+ flex-shrink: 0;
+ line-height: 28px;
+}
+
+.profile-field-row-display {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.profile-field-row-value {
+ flex: 1;
+ font-size: 14px;
+ color: var(--text);
+ line-height: 28px;
+ word-break: break-word;
+ text-align: left;
+}
+
+.profile-field-row-value.placeholder {
+ font-style: italic;
+ opacity: 0.7;
+}
+
+.profile-field-edit-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ flex-shrink: 0;
+ border: none;
+ background: transparent;
+ color: var(--muted);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ opacity: 0.7;
+}
+
+.profile-field-edit-btn:hover {
+ opacity: 1;
+ color: var(--blue);
+}
+
+.profile-field-row-input {
+ flex: 1;
+ padding: 4px 8px;
font-size: 14px;
font-family: inherit;
color: var(--text);
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
+ min-width: 0;
}
-.profile-dialog-field input:focus {
+.profile-field-row-input:focus {
outline: none;
border-color: var(--blue);
}
-.profile-dialog-readonly {
- padding: 9px 12px;
- font-size: 14px;
- color: var(--muted);
- background: var(--bg);
- border: 1px solid var(--border);
+.profile-field-row-actions {
+ display: flex;
+ gap: 6px;
+}
+
+.profile-field-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ height: 28px;
+ padding: 0 12px;
+ border: none;
border-radius: var(--radius-sm);
+ font-size: 12px;
+ font-weight: 600;
+ font-family: inherit;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: opacity 0.15s ease;
}
-.profile-dialog-actions {
- display: flex;
- justify-content: flex-end;
- gap: 10px;
- margin-top: 20px;
+.profile-field-btn--save {
+ background: var(--blue);
+ color: #ffffff;
+}
+
+.profile-field-btn--save:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.profile-field-btn--save:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.profile-field-btn--cancel {
+ background: var(--secondary);
+ color: #ffffff;
+}
+
+.profile-field-btn--cancel:hover {
+ opacity: 0.9;
}
diff --git a/tests/e2e/global-teardown.ts b/tests/e2e/global-teardown.ts
index 4fc0c15a..e8ca42c7 100644
--- a/tests/e2e/global-teardown.ts
+++ b/tests/e2e/global-teardown.ts
@@ -12,20 +12,22 @@ import {send} from './utils/api-request';
async function globalTeardown(): Promise
{
const username = process.env.TEST_USER_USERNAME;
- if (!username) return;
console.log('๐งน Deleting shared E2E test user...');
- const searchRes = await send('GET', `/users?attribute=username&value=${encodeURIComponent(username)}`);
+ const filter = `username eq "${username}"`;
+ const searchRes = await send('GET', `/users?filter=${encodeURIComponent(filter)}`);
if (!searchRes.ok) {
- console.warn(`โ ๏ธ Could not look up test user "${username}" for cleanup (HTTP ${searchRes.status})`);
- return;
+ throw new Error(
+ `Failed to look up test user "${username}" for cleanup: HTTP ${searchRes.status}: ${await searchRes.text()}`,
+ );
}
- const {users} = (await searchRes.json()) as {users?: {id: string}[]};
- const user = users?.[0];
- if (!user) {
+ const {users} = (await searchRes.json()) as {users?: {attributes?: {username?: string}; id: string}[]};
+ const matches = (users ?? []).filter((candidate) => candidate.attributes?.username === username);
+ if (matches.length === 0) {
console.warn(`โ ๏ธ Test user "${username}" not found โ nothing to clean up`);
return;
}
+ const user = matches[0];
const deleteRes = await send('DELETE', `/users/${user.id}`);
if (!deleteRes.ok) {
diff --git a/tests/e2e/pages/browser-quickstart.page.ts b/tests/e2e/pages/browser-quickstart.page.ts
index 675080ab..d5d113a4 100644
--- a/tests/e2e/pages/browser-quickstart.page.ts
+++ b/tests/e2e/pages/browser-quickstart.page.ts
@@ -11,6 +11,11 @@ import {Page, expect} from '@playwright/test';
import {GateLoginPage} from './gate-login.page';
import {Timeouts} from '../constants/timeouts';
+export const ProfileFieldKeys = {
+ familyName: 'family_name',
+ givenName: 'given_name',
+};
+
export class BrowserQuickstartPage extends GateLoginPage {
constructor(page: Page) {
super(page);
@@ -74,13 +79,24 @@ export class BrowserQuickstartPage extends GateLoginPage {
.waitFor({state: 'visible', timeout: Timeouts.ELEMENT_VISIBILITY});
}
- async fillProfileName(givenName: string, familyName: string): Promise {
- await this.page.locator('#profile-first-name').fill(givenName);
- await this.page.locator('#profile-last-name').fill(familyName);
+ /** Edits one field of the profile dialog, which renders each schema attribute as its own row
+ * with a pencil "Edit" button.*/
+ async editProfileField(fieldKey: string, value: string): Promise {
+ const row = this.page.locator(`.profile-field-row[data-field="${fieldKey}"]`);
+ await row.locator('[data-action="edit"]').click();
+ await row.locator('.profile-field-row-input').fill(value);
+ await row.locator('[data-action="save"]').click();
+ await expect(row.locator('.profile-field-row-input')).toHaveCount(0, {timeout: Timeouts.DEFAULT_ACTION});
+ }
+
+ /** Verifies a field's row reverted from edit mode back to display mode showing the just-saved value*/
+ async verifyProfileFieldValue(fieldKey: string, value: string): Promise {
+ const row = this.page.locator(`.profile-field-row[data-field="${fieldKey}"]`);
+ await expect(row.locator('.profile-field-row-value')).toHaveText(value, {timeout: Timeouts.ELEMENT_VISIBILITY});
}
- async saveProfile(): Promise {
- await this.page.locator('#profile-dialog-save').click();
+ async closeManageProfile(): Promise {
+ await this.page.locator('#profile-dialog-close').click();
await this.page.locator('#profile-dialog-overlay').waitFor({state: 'hidden', timeout: Timeouts.DEFAULT_ACTION});
}
diff --git a/tests/e2e/tests/browser-quickstart/sign-in-out.spec.ts b/tests/e2e/tests/browser-quickstart/sign-in-out.spec.ts
index a3665e82..64021adb 100644
--- a/tests/e2e/tests/browser-quickstart/sign-in-out.spec.ts
+++ b/tests/e2e/tests/browser-quickstart/sign-in-out.spec.ts
@@ -8,6 +8,7 @@
import {SampleApps, sampleAppUrl} from '../../constants/sample-apps';
import {expect, test} from '../../fixtures/sample-apps';
+import {ProfileFieldKeys} from '../../pages/browser-quickstart.page';
import {decodeJwtPayload} from '../../utils/jwt';
const appUrl = sampleAppUrl(SampleApps.BROWSER);
@@ -66,9 +67,13 @@ test.describe('browser/quickstart - Sign in and Sign out', () => {
await browserQuickstartPage.verifyLoggedIn();
await browserQuickstartPage.openManageProfile();
- await browserQuickstartPage.fillProfileName('Profile', 'Updated');
- await browserQuickstartPage.saveProfile();
+ await browserQuickstartPage.editProfileField(ProfileFieldKeys.givenName, 'Profile');
+ await browserQuickstartPage.editProfileField(ProfileFieldKeys.familyName, 'Updated');
+ await browserQuickstartPage.verifyProfileFieldValue(ProfileFieldKeys.givenName, 'Profile');
+ await browserQuickstartPage.verifyProfileFieldValue(ProfileFieldKeys.familyName, 'Updated');
+
+ await browserQuickstartPage.closeManageProfile();
await browserQuickstartPage.verifyDisplayedName('Profile Updated');
});
});