|
| 1 | +// @vitest-environment jsdom |
| 2 | +import { fireEvent, render, screen } from '@testing-library/react'; |
| 3 | +import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| 4 | +import { NoRuntimeBand } from './NoRuntimeBand'; |
| 5 | +import { NO_RUNTIME_BAND } from '../../lib/positioning'; |
| 6 | + |
| 7 | +const trackCtaClickMock = vi.hoisted(() => vi.fn()); |
| 8 | +vi.mock('../../lib/analytics/client', () => ({ |
| 9 | + track: vi.fn(), |
| 10 | + trackCtaClick: trackCtaClickMock, |
| 11 | + trackExternalLinkClick: vi.fn(), |
| 12 | +})); |
| 13 | + |
| 14 | +beforeEach(() => trackCtaClickMock.mockClear()); |
| 15 | + |
| 16 | +describe('NoRuntimeBand', () => { |
| 17 | + it('makes the two-word headline the section heading and names the section by it', () => { |
| 18 | + const { container } = render(<NoRuntimeBand />); |
| 19 | + const heading = screen.getByRole('heading', { level: 2 }); |
| 20 | + expect(heading.textContent).toBe(NO_RUNTIME_BAND.headline); |
| 21 | + expect(heading.id).toBe('no-runtime-heading'); |
| 22 | + const section = container.querySelector('[data-ui="section"]'); |
| 23 | + expect(section?.getAttribute('aria-labelledby')).toBe('no-runtime-heading'); |
| 24 | + expect(section?.getAttribute('id')).toBe('no-runtime'); |
| 25 | + expect(container.querySelector('.no-runtime-eyebrow')?.textContent).toBe(NO_RUNTIME_BAND.eyebrow); |
| 26 | + expect(screen.getByText(NO_RUNTIME_BAND.body)).toBeTruthy(); |
| 27 | + }); |
| 28 | + |
| 29 | + it('sits on the dark surface at the full rhythm, like Fork us', () => { |
| 30 | + const { container } = render(<NoRuntimeBand />); |
| 31 | + const section = container.querySelector('[data-ui="section"]'); |
| 32 | + expect(section?.getAttribute('data-surface')).toBe('dark'); |
| 33 | + expect(section?.getAttribute('data-tight')).toBeNull(); |
| 34 | + expect(section?.classList.contains('no-runtime')).toBe(true); |
| 35 | + }); |
| 36 | + |
| 37 | + it('offers one text link and no button, and reports it as a homepage CTA', () => { |
| 38 | + const { container } = render(<NoRuntimeBand />); |
| 39 | + const links = container.querySelectorAll('a'); |
| 40 | + expect(links).toHaveLength(1); |
| 41 | + const link = screen.getByRole('link', { name: NO_RUNTIME_BAND.link.label }); |
| 42 | + expect(link.getAttribute('href')).toBe(NO_RUNTIME_BAND.link.href); |
| 43 | + expect(container.querySelector('[data-ui="button"]')).toBeNull(); |
| 44 | + fireEvent.click(link); |
| 45 | + expect(trackCtaClickMock).toHaveBeenCalledWith({ |
| 46 | + cta_id: 'home_no_runtime_docs', |
| 47 | + track: 'developer', |
| 48 | + surface: 'home', |
| 49 | + destination_url: NO_RUNTIME_BAND.link.href, |
| 50 | + }); |
| 51 | + }); |
| 52 | + |
| 53 | + it('draws both flows from the copy module, with the ghost hop only in the usual one', () => { |
| 54 | + const { container } = render(<NoRuntimeBand />); |
| 55 | + const usual = container.querySelector('[data-flow="usual"]'); |
| 56 | + const ours = container.querySelector('[data-flow="ours"]'); |
| 57 | + expect( |
| 58 | + Array.from(usual!.querySelectorAll('.no-runtime-node')).map((n) => n.textContent), |
| 59 | + ).toEqual([...NO_RUNTIME_BAND.flows.usual.nodes]); |
| 60 | + expect( |
| 61 | + Array.from(ours!.querySelectorAll('.no-runtime-node')).map((n) => n.textContent), |
| 62 | + ).toEqual([...NO_RUNTIME_BAND.flows.ours.nodes]); |
| 63 | + expect(container.querySelectorAll('.no-runtime-node.is-ghost')).toHaveLength(1); |
| 64 | + expect(usual!.querySelector('.no-runtime-node.is-ghost')?.textContent).toBe( |
| 65 | + NO_RUNTIME_BAND.flows.usual.ghost, |
| 66 | + ); |
| 67 | + expect(ours!.querySelector('.is-ghost')).toBeNull(); |
| 68 | + // The direct arrow spans the hop that is not there: one in ours, none in usual. |
| 69 | + const oursArrows = ours!.querySelectorAll('.no-runtime-arrow'); |
| 70 | + expect(oursArrows).toHaveLength(1); |
| 71 | + expect(oursArrows[0].classList.contains('is-direct')).toBe(true); |
| 72 | + const usualArrows = usual!.querySelectorAll('.no-runtime-arrow'); |
| 73 | + expect(usualArrows).toHaveLength(2); |
| 74 | + expect(usual!.querySelector('.no-runtime-arrow.is-direct')).toBeNull(); |
| 75 | + }); |
| 76 | + |
| 77 | + it('gives the flows a prose caption and hides the arrows from assistive tech', () => { |
| 78 | + const { container } = render(<NoRuntimeBand />); |
| 79 | + const figure = container.querySelector('figure.no-runtime-figure'); |
| 80 | + expect(figure).toBeTruthy(); |
| 81 | + expect(figure?.querySelector('figcaption')?.textContent).toBe(NO_RUNTIME_BAND.figureCaption); |
| 82 | + // getByRole skips hidden subtrees, so aria-hiding the figure fails here. |
| 83 | + // (jsdom's accessible-name computation gives a figure no name from its |
| 84 | + // figcaption, so the name is not queried; the caption is checked below.) |
| 85 | + expect(screen.getByRole('figure')).toBe(figure); |
| 86 | + expect(figure?.querySelector('figcaption')?.closest('[aria-hidden="true"]')).toBeNull(); |
| 87 | + // The drawn flows duplicate the caption, so they are hidden as a unit. |
| 88 | + expect(figure?.querySelector('.no-runtime-flows')?.getAttribute('aria-hidden')).toBe('true'); |
| 89 | + }); |
| 90 | + |
| 91 | + it('carries no runway of its own, so it does not merge with Fork us below it', () => { |
| 92 | + // Two stripes 450px apart read as one tall block in the browser; the |
| 93 | + // marking stays on Fork us alone (spec §5 fallback). |
| 94 | + const { container } = render(<NoRuntimeBand />); |
| 95 | + expect(container.querySelector('.no-runtime-runway')).toBeNull(); |
| 96 | + // Structurally, not by class name: the section holds the container and |
| 97 | + // nothing else, so no stripe can come back under another name. |
| 98 | + const section = container.querySelector('[data-ui="section"]'); |
| 99 | + expect(section?.children).toHaveLength(1); |
| 100 | + expect(section?.firstElementChild?.getAttribute('data-ui')).toBe('container'); |
| 101 | + }); |
| 102 | +}); |
0 commit comments