import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { expect, test, vi } from 'vitest'
import { PRESETS } from '../../presets.generated'
import { ThemeEditor } from '../ThemeEditor'
import { fromShareHash, toProfileJson } from '../model/codec'
const nameBox = () => screen.getByRole('textbox', { name: 'Название темы' })
test('undo and redo walk the history from the header buttons and from Ctrl+Z / Ctrl+Shift+Z', async () => {
const user = userEvent.setup()
render()
const undo = screen.getByRole('button', { name: 'Отменить' })
const redo = screen.getByRole('button', { name: 'Вернуть' })
expect(undo).toBeDisabled()
// A preset pick starts a fresh history; an edit (typing, one coalesced step) is what undo reverts.
const edited = `${PRESETS[0].name} 2`
await user.type(nameBox(), ' 2')
expect(nameBox()).toHaveValue(edited)
expect(undo).toBeEnabled()
await user.click(undo)
expect(nameBox()).toHaveValue(PRESETS[0].name)
await user.click(redo)
expect(nameBox()).toHaveValue(edited)
await user.keyboard('{Control>}z{/Control}')
expect(nameBox()).toHaveValue(PRESETS[0].name)
await user.keyboard('{Control>}{Shift>}z{/Shift}{/Control}')
expect(nameBox()).toHaveValue(edited)
})
test('reset names the theme it returns to and restores it', async () => {
const user = userEvent.setup()
render()
await user.type(nameBox(), ' edited')
await user.click(screen.getByRole('button', { name: `Сбросить до «${PRESETS[0].name}»` }))
expect(nameBox()).toHaveValue(PRESETS[0].name)
})
test('renaming the theme updates the exported name and id', async () => {
const user = userEvent.setup()
render()
await user.clear(nameBox())
await user.type(nameBox(), 'My Night')
await user.click(screen.getByRole('button', { name: 'Скопировать для мода' }))
expect(JSON.parse(await navigator.clipboard.readText()).name).toBe('My Night')
})
test('copy puts the profile JSON on the clipboard and the button confirms', async () => {
const user = userEvent.setup()
render()
await user.click(screen.getByRole('button', { name: 'Скопировать для мода' }))
expect(await navigator.clipboard.readText()).toBe(toProfileJson(PRESETS[1]))
expect(screen.getByRole('button', { name: 'Скопировано' })).toBeInTheDocument()
expect(screen.getByRole('status')).toHaveTextContent('Скопировано')
})
test('share copies a link whose hash decodes back to the draft', async () => {
const user = userEvent.setup()
render()
await user.click(screen.getByRole('button', { name: 'Скопировать ссылку' }))
const link = await navigator.clipboard.readText()
const hash = /#t=(.+)$/.exec(link)?.[1] ?? ''
expect(fromShareHash(hash)).toMatchObject({ name: PRESETS[4].name, theme: PRESETS[4].theme })
expect(screen.getByRole('button', { name: 'Ссылка скопирована' })).toBeInTheDocument()
})
test('a blocked clipboard says what to do instead', async () => {
const user = userEvent.setup()
render()
vi.spyOn(navigator.clipboard, 'writeText').mockRejectedValueOnce(new Error('denied'))
await user.click(screen.getByRole('button', { name: 'Скопировать для мода' }))
expect(await screen.findByRole('status')).toHaveTextContent('Скачай .json')
expect(screen.getByRole('button', { name: 'Скопировать для мода' })).toBeInTheDocument()
})
test('download saves the profile JSON as .json', async () => {
const user = userEvent.setup()
const createUrl = vi.fn(() => 'blob:theme')
Object.assign(URL, { createObjectURL: createUrl, revokeObjectURL: vi.fn() })
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (this: HTMLAnchorElement) {
expect(this.download).toBe(`${PRESETS[0].id}.json`)
})
render()
await user.click(screen.getByRole('button', { name: 'Скачать .json' }))
expect(click).toHaveBeenCalledOnce()
const blob = (createUrl.mock.calls[0] as unknown as [Blob])[0]
expect(await blob.text()).toBe(toProfileJson(PRESETS[0]))
click.mockRestore()
})
test('loading a file imports a valid theme and explains a broken one', async () => {
const user = userEvent.setup()
const { container } = render()
const input = container.querySelector('input[type="file"]')!
await user.upload(input, new File([toProfileJson(PRESETS[5])], 'theme.json', { type: 'application/json' }))
expect(await screen.findByRole('status')).toHaveTextContent(`Загружена тема «${PRESETS[5].name}»`)
expect(nameBox()).toHaveValue(PRESETS[5].name)
await user.upload(input, new File(['not json'], 'broken.json', { type: 'application/json' }))
expect(await screen.findByText(/Это не тема LoVisual/)).toBeInTheDocument()
expect(nameBox()).toHaveValue(PRESETS[5].name)
})