Preserves the working F3 Phaser demo implementation before resetting the main frontend/ directory for a fresh start. The POC demonstrates: - Vue 3 + Phaser 3 integration - Real card rendering with images - Vue-Phaser state sync via gameBridge - Card interactions and damage counters To restore: copy .claude/frontend-poc/ back to frontend/ and run npm install
388 lines
11 KiB
TypeScript
388 lines
11 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
|
|
import DisplayNameEditor from './DisplayNameEditor.vue'
|
|
|
|
describe('DisplayNameEditor', () => {
|
|
describe('view mode', () => {
|
|
it('displays the current name', () => {
|
|
/**
|
|
* Test that the display name is shown in view mode.
|
|
*
|
|
* By default, the component shows the current name
|
|
* with an edit button, not an input field.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
expect(wrapper.text()).toContain('Test User')
|
|
})
|
|
|
|
it('shows edit button', () => {
|
|
/**
|
|
* Test that an edit button is visible in view mode.
|
|
*
|
|
* Users need a way to enter edit mode to change their name.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
const editButton = wrapper.find('button')
|
|
expect(editButton.exists()).toBe(true)
|
|
})
|
|
|
|
it('enters edit mode when edit button is clicked', async () => {
|
|
/**
|
|
* Test entering edit mode.
|
|
*
|
|
* Clicking the edit button should switch from viewing
|
|
* to editing the display name.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
|
|
// Should now show an input field
|
|
expect(wrapper.find('input').exists()).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('edit mode', () => {
|
|
it('shows input with current value', async () => {
|
|
/**
|
|
* Test that input is pre-filled with current name.
|
|
*
|
|
* When entering edit mode, the input should contain
|
|
* the current display name for easy editing.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
// Enter edit mode
|
|
await wrapper.find('button').trigger('click')
|
|
|
|
const input = wrapper.find('input')
|
|
expect((input.element as HTMLInputElement).value).toBe('Test User')
|
|
})
|
|
|
|
it('shows save and cancel buttons', async () => {
|
|
/**
|
|
* Test that save and cancel buttons are visible.
|
|
*
|
|
* Users need clear actions to either save changes
|
|
* or cancel and revert to the original value.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
|
|
const buttons = wrapper.findAll('button')
|
|
const buttonTexts = buttons.map(b => b.text())
|
|
|
|
expect(buttonTexts.some(t => t.includes('Save'))).toBe(true)
|
|
expect(buttonTexts.some(t => t.includes('Cancel'))).toBe(true)
|
|
})
|
|
|
|
it('shows character count', async () => {
|
|
/**
|
|
* Test that character count is displayed.
|
|
*
|
|
* Users should know how many characters they've used
|
|
* relative to the maximum allowed.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
|
|
expect(wrapper.text()).toContain('/32')
|
|
})
|
|
|
|
it('cancels and reverts on cancel button click', async () => {
|
|
/**
|
|
* Test cancel functionality.
|
|
*
|
|
* Clicking cancel should exit edit mode and discard
|
|
* any changes made to the input.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
// Enter edit mode
|
|
await wrapper.find('button').trigger('click')
|
|
|
|
// Change the value
|
|
await wrapper.find('input').setValue('New Name')
|
|
|
|
// Click cancel
|
|
const cancelButton = wrapper.findAll('button').find(b => b.text().includes('Cancel'))
|
|
await cancelButton!.trigger('click')
|
|
|
|
// Should be back in view mode with original name
|
|
expect(wrapper.find('input').exists()).toBe(false)
|
|
expect(wrapper.text()).toContain('Test User')
|
|
})
|
|
|
|
it('cancels on Escape key', async () => {
|
|
/**
|
|
* Test keyboard shortcut for cancel.
|
|
*
|
|
* Pressing Escape should cancel editing as a
|
|
* common keyboard shortcut.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
await wrapper.find('input').trigger('keydown', { key: 'Escape' })
|
|
|
|
expect(wrapper.find('input').exists()).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('validation', () => {
|
|
it('shows error for empty name', async () => {
|
|
/**
|
|
* Test validation of empty display name.
|
|
*
|
|
* Empty or whitespace-only names should be rejected
|
|
* with a clear error message.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
await wrapper.find('input').setValue('')
|
|
|
|
// Click save
|
|
const saveButton = wrapper.findAll('button').find(b => b.text().includes('Save'))
|
|
await saveButton!.trigger('click')
|
|
|
|
expect(wrapper.text()).toContain('cannot be empty')
|
|
})
|
|
|
|
it('shows error for name shorter than 2 characters', async () => {
|
|
/**
|
|
* Test minimum length validation.
|
|
*
|
|
* Very short names are likely typos and should be rejected.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
await wrapper.find('input').setValue('A')
|
|
|
|
const saveButton = wrapper.findAll('button').find(b => b.text().includes('Save'))
|
|
await saveButton!.trigger('click')
|
|
|
|
expect(wrapper.text()).toContain('at least 2 characters')
|
|
})
|
|
|
|
it('shows error for name longer than 32 characters', async () => {
|
|
/**
|
|
* Test maximum length validation.
|
|
*
|
|
* Names that are too long should be rejected to prevent
|
|
* UI issues and database constraints.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
await wrapper.find('input').setValue('A'.repeat(33))
|
|
|
|
const saveButton = wrapper.findAll('button').find(b => b.text().includes('Save'))
|
|
await saveButton!.trigger('click')
|
|
|
|
expect(wrapper.text()).toContain('cannot exceed 32')
|
|
})
|
|
|
|
it('does not emit save with validation errors', async () => {
|
|
/**
|
|
* Test that invalid input does not trigger save.
|
|
*
|
|
* The component should validate locally before emitting
|
|
* the save event to avoid unnecessary API calls.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
await wrapper.find('input').setValue('')
|
|
|
|
const saveButton = wrapper.findAll('button').find(b => b.text().includes('Save'))
|
|
await saveButton!.trigger('click')
|
|
|
|
expect(wrapper.emitted('save')).toBeFalsy()
|
|
})
|
|
})
|
|
|
|
describe('saving', () => {
|
|
it('emits save event with trimmed value', async () => {
|
|
/**
|
|
* Test save event emission.
|
|
*
|
|
* When the user saves, the component should emit the
|
|
* trimmed display name for the parent to handle.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
await wrapper.find('input').setValue(' New Name ')
|
|
|
|
const saveButton = wrapper.findAll('button').find(b => b.text().includes('Save'))
|
|
await saveButton!.trigger('click')
|
|
|
|
expect(wrapper.emitted('save')).toBeTruthy()
|
|
expect(wrapper.emitted('save')![0]).toEqual(['New Name'])
|
|
})
|
|
|
|
it('saves on Enter key', async () => {
|
|
/**
|
|
* Test keyboard shortcut for save.
|
|
*
|
|
* Pressing Enter should save the changes as a
|
|
* common keyboard shortcut.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
await wrapper.find('input').setValue('New Name')
|
|
await wrapper.find('input').trigger('keydown', { key: 'Enter' })
|
|
|
|
expect(wrapper.emitted('save')).toBeTruthy()
|
|
})
|
|
|
|
it('disables buttons while saving', async () => {
|
|
/**
|
|
* Test button states during save.
|
|
*
|
|
* While saving, buttons should be disabled to prevent
|
|
* duplicate submissions.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: true,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
|
|
const buttons = wrapper.findAll('button')
|
|
buttons.forEach(button => {
|
|
if (button.text().includes('Save') || button.text().includes('Cancel')) {
|
|
expect(button.attributes('disabled')).toBeDefined()
|
|
}
|
|
})
|
|
})
|
|
|
|
it('shows "Saving..." text while saving', async () => {
|
|
/**
|
|
* Test loading state display.
|
|
*
|
|
* Users should see feedback that their save is in progress.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: true,
|
|
},
|
|
})
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
|
|
expect(wrapper.text()).toContain('Saving')
|
|
})
|
|
|
|
it('exits edit mode when modelValue matches saved value', async () => {
|
|
/**
|
|
* Test automatic exit from edit mode on success.
|
|
*
|
|
* When the parent updates the modelValue to match the
|
|
* saved value, edit mode should close automatically.
|
|
*/
|
|
const wrapper = mount(DisplayNameEditor, {
|
|
props: {
|
|
modelValue: 'Test User',
|
|
isSaving: false,
|
|
},
|
|
})
|
|
|
|
// Enter edit mode and save
|
|
await wrapper.find('button').trigger('click')
|
|
await wrapper.find('input').setValue('New Name')
|
|
|
|
const saveButton = wrapper.findAll('button').find(b => b.text().includes('Save'))
|
|
await saveButton!.trigger('click')
|
|
|
|
// Simulate parent updating the value
|
|
await wrapper.setProps({ modelValue: 'New Name' })
|
|
|
|
// Should exit edit mode
|
|
expect(wrapper.find('input').exists()).toBe(false)
|
|
})
|
|
})
|
|
})
|