feat: integration screen
This commit is contained in:
1 parent
a9a208b80a
commit
ad55137883
13 files changed
+786
-6
No files matched your search
@@ -0,0 +1,189 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import uiPlugin from '@nuxt/ui/vue-plugin'
|
||||
import router from '@/router'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import EndpointCard from '@/components/EndpointCard.vue'
|
||||
import IntegrationView from '../IntegrationView.vue'
|
||||
|
||||
const STUB_ORIGIN = 'https://cms.renew.example'
|
||||
|
||||
let currentWrapper: Awaited<ReturnType<typeof mount>> | null = null
|
||||
|
||||
function stubOrigin(origin: string) {
|
||||
vi.stubGlobal('location', { ...window.location, origin })
|
||||
}
|
||||
|
||||
function stubClipboard() {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText },
|
||||
})
|
||||
return writeText
|
||||
}
|
||||
|
||||
async function mountIntegration() {
|
||||
setActivePinia(createPinia())
|
||||
const session = useSessionStore()
|
||||
session.session = { verified: true, username: 'marta' }
|
||||
session.status = 'ready'
|
||||
await router.push('/integration')
|
||||
const wrapper = mount(IntegrationView, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
plugins: [router, uiPlugin],
|
||||
},
|
||||
})
|
||||
currentWrapper = wrapper
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
function findButton(wrapper: Awaited<ReturnType<typeof mount>>, name: string) {
|
||||
return wrapper.findAll('button').find((button) => button.text().trim() === name)
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
currentWrapper?.unmount()
|
||||
currentWrapper = null
|
||||
vi.unstubAllGlobals()
|
||||
vi.useRealTimers()
|
||||
await router.push('/login')
|
||||
})
|
||||
|
||||
describe("Default — the page loads with the app's own origin", () => {
|
||||
it('renders the header, base URL, endpoint cards and notes with the stubbed origin', async () => {
|
||||
stubOrigin(STUB_ORIGIN)
|
||||
const wrapper = await mountIntegration()
|
||||
|
||||
expect(wrapper.text()).toContain('Connect your website')
|
||||
expect(wrapper.text()).toContain('A guide for the developer who builds your public site.')
|
||||
expect(wrapper.text()).toContain(
|
||||
"RenewCMS is headless: it stores and publishes your articles but doesn't show them to visitors.",
|
||||
)
|
||||
|
||||
expect(wrapper.text()).toContain(STUB_ORIGIN)
|
||||
expect(findButton(wrapper, 'Copy')).toBeTruthy()
|
||||
|
||||
const cardTexts = wrapper.findAllComponents(EndpointCard).map((c) => c.text())
|
||||
expect(cardTexts).toHaveLength(2)
|
||||
expect(cardTexts[0]).toContain('/v1/article/')
|
||||
expect(cardTexts[0]).toContain('Lists every online article.')
|
||||
expect(cardTexts[0]).toContain(`${STUB_ORIGIN}/v1/article/`)
|
||||
expect(cardTexts[1]).toContain('/v1/article/{id}')
|
||||
expect(cardTexts[1]).toContain(
|
||||
'Returns one online article. Drafts and unknown ids both return 404.',
|
||||
)
|
||||
expect(cardTexts[1]).toContain(`${STUB_ORIGIN}/v1/article/12`)
|
||||
|
||||
const fetchTabs = wrapper.findAll('[role="tab"]').filter((tab) => tab.text() === 'fetch')
|
||||
expect(fetchTabs.length).toBeGreaterThan(0)
|
||||
fetchTabs.forEach((tab) => {
|
||||
expect(tab.attributes('aria-selected')).toBe('true')
|
||||
})
|
||||
|
||||
expect(wrapper.text()).toContain(
|
||||
'Anonymous callers only ever get online articles. Drafts stay private until you publish them.',
|
||||
)
|
||||
expect(wrapper.text()).toContain(
|
||||
'Images in body use paths like /static/uploadedImages/<uuid>.png. Prefix them with the base URL before rendering.',
|
||||
)
|
||||
expect(wrapper.text()).toContain(
|
||||
"Browsers can only call the API from origins listed in the server's CORS setting. Ask whoever runs the server to add your site's address.",
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the breadcrumb Integration', async () => {
|
||||
stubOrigin(STUB_ORIGIN)
|
||||
const wrapper = await mountIntegration()
|
||||
expect(wrapper.text()).toContain('Integration')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Copied — the Base URL copy button', () => {
|
||||
it('writes the origin to the clipboard, shows Copied for 2s, then reverts', async () => {
|
||||
stubOrigin(STUB_ORIGIN)
|
||||
const writeText = stubClipboard()
|
||||
vi.useFakeTimers()
|
||||
const wrapper = await mountIntegration()
|
||||
|
||||
const copyButton = findButton(wrapper, 'Copy')!
|
||||
await copyButton.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(writeText).toHaveBeenCalledWith(STUB_ORIGIN)
|
||||
expect(findButton(wrapper, 'Copied')).toBeTruthy()
|
||||
expect(wrapper.find('[aria-live="polite"]').text()).toContain('Copied')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
|
||||
expect(findButton(wrapper, 'Copy')).toBeTruthy()
|
||||
expect(findButton(wrapper, 'Copied')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Each EndpointCard's own Copy button is independent of the Base URL's", () => {
|
||||
it("copies only that card's code and leaves the other buttons unaffected", async () => {
|
||||
stubOrigin(STUB_ORIGIN)
|
||||
const writeText = stubClipboard()
|
||||
const wrapper = await mountIntegration()
|
||||
|
||||
const cards = wrapper.findAllComponents(EndpointCard)
|
||||
const firstCardCopyButton = cards[0]
|
||||
.findAll('button')
|
||||
.find((button) => button.text().trim() === 'Copy')!
|
||||
await firstCardCopyButton.trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
const expectedCode =
|
||||
`const res = await fetch('${STUB_ORIGIN}/v1/article/')\n` +
|
||||
'const articles = await res.json()'
|
||||
expect(writeText).toHaveBeenCalledWith(expectedCode)
|
||||
|
||||
const copyButtons = wrapper.findAll('button').filter((button) => {
|
||||
const text = button.text().trim()
|
||||
return text === 'Copy' || text === 'Copied'
|
||||
})
|
||||
const copiedButtons = copyButtons.filter((button) => button.text().trim() === 'Copied')
|
||||
expect(copiedButtons).toHaveLength(1)
|
||||
|
||||
const baseUrlCopyButton = findButton(wrapper, 'Copy')
|
||||
expect(baseUrlCopyButton).toBeTruthy()
|
||||
|
||||
const secondCardCopyButton = cards[1]
|
||||
.findAll('button')
|
||||
.find((button) => button.text().trim() === 'Copy')
|
||||
expect(secondCardCopyButton).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
async function activateTab(container: HTMLElement, label: string) {
|
||||
const button = Array.from(container.querySelectorAll('[role="tab"]')).find(
|
||||
(el) => el.textContent?.trim() === label,
|
||||
) as HTMLButtonElement | undefined
|
||||
button?.focus()
|
||||
button?.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }),
|
||||
)
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
describe('curl and Response tabs render, unchecked visually', () => {
|
||||
it('shows the curl command and the Response JSON sample built from the real Article fields', async () => {
|
||||
stubOrigin(STUB_ORIGIN)
|
||||
const wrapper = await mountIntegration()
|
||||
|
||||
const firstCard = wrapper.findAllComponents(EndpointCard)[0]
|
||||
await activateTab(firstCard.element as HTMLElement, 'curl')
|
||||
|
||||
expect(firstCard.text()).toContain(`curl ${STUB_ORIGIN}/v1/article/`)
|
||||
|
||||
await activateTab(firstCard.element as HTMLElement, 'Response')
|
||||
|
||||
expect(firstCard.text()).toContain('"is_online"')
|
||||
expect(firstCard.text()).toContain('"created_at"')
|
||||
expect(firstCard.text()).toContain('"id"')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user