test(admin): PluginsView and settings paths
- PluginsView: tab switching, one request per typing burst and a blank search without q, name fallback on cards, copied state clearing, a refused clipboard write, a failed marketplace request - useSettings: settingsPath prefers path, then controller, then the singleton form; settingsEntryForPath
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
setSettings,
|
||||
settings,
|
||||
settingsEntry,
|
||||
settingsEntryForPath,
|
||||
settingsGroups,
|
||||
settingsLinkFor,
|
||||
settingsPath,
|
||||
@@ -56,6 +57,25 @@ describe('useSettings (D-21)', () => {
|
||||
expect(useSettings().settingsPath).toBe(settingsPath)
|
||||
})
|
||||
|
||||
it('prefers an entry path, then its controller, then the singleton form (34-05)', () => {
|
||||
const base = settingsFixture.list.data[0]!
|
||||
expect(settingsPath({ ...base, code: 'plugins', model: '', controller: 'acme.demo.locales', path: '/system/plugins' })).toBe(
|
||||
'/system/plugins',
|
||||
)
|
||||
expect(settingsPath({ ...base, code: 'plugins', model: '', controller: 'acme.demo.locales', path: '' })).toBe('/acme/demo/locales')
|
||||
expect(settingsPath({ ...base, code: 'mail', controller: '', path: undefined })).toBe('/settings/mail')
|
||||
expect(settingsPath({ ...base, code: 'mail', controller: '', path: '' })).toBe('/settings/mail')
|
||||
})
|
||||
|
||||
it('finds the entry of an admin screen by its path', () => {
|
||||
const plugins = { ...settingsFixture.list.data[0]!, code: 'plugins', model: '', controller: '', path: '/system/plugins' }
|
||||
setSettings([...settingsFixture.list.data, plugins])
|
||||
expect(settingsEntryForPath('/system/plugins')?.code).toBe('plugins')
|
||||
expect(settingsEntryForPath('/system/other')).toBeNull()
|
||||
expect(settingsEntryForPath('')).toBeNull()
|
||||
expect(useSettings().settingsEntryForPath).toBe(settingsEntryForPath)
|
||||
})
|
||||
|
||||
it('finds the link entry that opens a controller, never a singleton', () => {
|
||||
const link = { ...settingsFixture.list.data[0]!, code: 'locales', model: '', controller: 'acme.demo.locales' }
|
||||
setSettings([...settingsFixture.list.data, link])
|
||||
|
||||
@@ -259,6 +259,100 @@ describe('PluginsView', () => {
|
||||
expect(wrapper.find('[data-market-empty]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('switches between the Installed and Marketplace tabs', async () => {
|
||||
const { wrapper } = await openPlugins(withUpdate(), {
|
||||
[`GET ${MARKET}`]: marketBody([marketPlugin({ id: 'acme.journal', name: 'Journal' })]),
|
||||
})
|
||||
expect(wrapper.find('[data-tab="installed"]').attributes('data-state')).toBe('active')
|
||||
expect(wrapper.find('[data-plugin="acme.blog"]').isVisible()).toBe(true)
|
||||
await openMarketplace(wrapper)
|
||||
expect(wrapper.find('[data-tab="marketplace"]').attributes('data-state')).toBe('active')
|
||||
expect(wrapper.find('[data-market-plugin="acme.journal"]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-plugin="acme.blog"]').exists()).toBe(false)
|
||||
await wrapper.find('[data-tab="installed"]').trigger('mousedown', { button: 0 })
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-plugin="acme.blog"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('sends one search request for a burst of typing', async () => {
|
||||
const { wrapper, calls } = await openPlugins(withUpdate(), {
|
||||
[`GET ${MARKET}`]: (request) => marketBody([marketPlugin({ id: `acme.${queryOf(request).get('q') ?? 'all'}` })]),
|
||||
})
|
||||
await openMarketplace(wrapper)
|
||||
expect(requestsTo(calls, 'GET', MARKET)).toHaveLength(1)
|
||||
const search = wrapper.find('[data-market-search]')
|
||||
for (const value of ['b', 'bl', 'blo', 'blog']) {
|
||||
await search.setValue(value)
|
||||
await wait(50)
|
||||
}
|
||||
expect(requestsTo(calls, 'GET', MARKET)).toHaveLength(1)
|
||||
await wait(350)
|
||||
await flushPromises()
|
||||
const searches = requestsTo(calls, 'GET', MARKET)
|
||||
expect(searches).toHaveLength(2)
|
||||
expect(queryOf(searches[1]!).get('q')).toBe('blog')
|
||||
// A blank search asks for everything, without a q parameter.
|
||||
await search.setValue(' ')
|
||||
await wait(350)
|
||||
await flushPromises()
|
||||
const blank = requestsTo(calls, 'GET', MARKET)
|
||||
expect(blank).toHaveLength(3)
|
||||
expect(queryOf(blank[2]!).has('q')).toBe(false)
|
||||
})
|
||||
|
||||
it('falls back to the ID when a plugin has no name', async () => {
|
||||
const { wrapper } = await openPlugins(withUpdate(), {
|
||||
[`GET ${MARKET}`]: marketBody([marketPlugin({ id: 'acme.nameless', name: '', latestVersion: '' })]),
|
||||
})
|
||||
expect(wrapper.find('[data-plugin="acme.translate"] [data-plugin-name]').text()).toBe('acme.translate')
|
||||
await openMarketplace(wrapper)
|
||||
const card = wrapper.find('[data-market-plugin="acme.nameless"]')
|
||||
expect(card.find('[data-market-name]').text()).toBe('acme.nameless')
|
||||
expect(card.find('[data-market-version]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('copies an install command and clears the copied state after two seconds', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
|
||||
const { wrapper } = await openPlugins(withUpdate(), {
|
||||
[`GET ${MARKET}`]: marketBody([marketPlugin({ id: 'acme.journal' }), marketPlugin({ id: 'acme.shop' })]),
|
||||
})
|
||||
await openMarketplace(wrapper)
|
||||
const journal = () => wrapper.find('[data-market-plugin="acme.journal"] [data-action="copy-command"]')
|
||||
const shop = () => wrapper.find('[data-market-plugin="acme.shop"] [data-action="copy-command"]')
|
||||
await journal().trigger('click')
|
||||
await flushPromises()
|
||||
expect(writeText).toHaveBeenCalledExactlyOnceWith('summer plugin:install acme.journal')
|
||||
expect(journal().text()).toBe('Skopiowano')
|
||||
expect(shop().text()).toBe('Kopiuj')
|
||||
await wait(2100)
|
||||
await flushPromises()
|
||||
expect(journal().text()).toBe('Kopiuj')
|
||||
})
|
||||
|
||||
it('falls back when the clipboard refuses the write', async () => {
|
||||
const writeText = vi.fn().mockRejectedValue(new Error('denied'))
|
||||
Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } })
|
||||
const { wrapper } = await openPlugins(withUpdate(), {
|
||||
[`GET ${MARKET}`]: marketBody([marketPlugin({ id: 'acme.journal' })]),
|
||||
})
|
||||
await openMarketplace(wrapper)
|
||||
const card = wrapper.find('[data-market-plugin="acme.journal"]')
|
||||
await card.find('[data-action="copy-command"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(card.find('[data-copy-failed]').exists()).toBe(true)
|
||||
expect(document.activeElement).toBe(card.find('[data-command="install:acme.journal"]').element)
|
||||
})
|
||||
|
||||
it('shows the registry notice when the marketplace request itself fails', async () => {
|
||||
const { wrapper } = await openPlugins(withUpdate(), {
|
||||
[`GET ${MARKET}`]: { status: 500, body: { error: { code: 'internal', message: 'x', details: {} } } },
|
||||
})
|
||||
await openMarketplace(wrapper)
|
||||
expect(wrapper.find('[data-market-unavailable]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-market-empty]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('shows the registry notice when the marketplace search fails', async () => {
|
||||
const { wrapper } = await openPlugins(withUpdate(), {
|
||||
[`GET ${MARKET}`]: marketBody([], 'unavailable'),
|
||||
|
||||
Reference in New Issue
Block a user