88 lines
3.5 KiB
JavaScript
88 lines
3.5 KiB
JavaScript
import React from 'react';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
|
|
jest.mock('@/assets/images/doormile-mark.png', () => 'doormile-mark.png', { virtual: true });
|
|
jest.mock('lucide-react', () =>
|
|
new Proxy({}, {
|
|
get: (_t, prop) => (prop === '__esModule' ? true : (props) => <span data-testid={`icon-${String(prop)}`} {...props} />),
|
|
})
|
|
);
|
|
|
|
import AILauncher from '@/components/assistant/DoormileAI/AILauncher';
|
|
|
|
/**
|
|
* The floating control that opens MileTruth, in place of the bare 32px brand
|
|
* mark that used to sit in the header between search and the bell.
|
|
*
|
|
* Two things here are requirements rather than styling, and both are the kind
|
|
* that a refactor silently undoes: the outline is the brand crimson on hover
|
|
* and on keyboard focus, and the button steps aside while the panel is open
|
|
* instead of sitting on top of a docked panel.
|
|
*/
|
|
const setup = (props = {}) => {
|
|
const onToggle = jest.fn();
|
|
const utils = render(<AILauncher isOpen={false} onToggle={onToggle} {...props} />);
|
|
return { ...utils, onToggle };
|
|
};
|
|
|
|
describe('the MileTruth launcher', () => {
|
|
it('is named, not just a mark', () => {
|
|
// The header version was an unlabelled logo among utility icons, with
|
|
// nothing on screen saying what it opened.
|
|
setup();
|
|
expect(screen.getByText('Doormile')).toBeInTheDocument();
|
|
});
|
|
|
|
it('opens the assistant when clicked', () => {
|
|
const { onToggle } = setup();
|
|
fireEvent.click(screen.getByRole('button', { name: /open miletruth/i }));
|
|
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('outlines in brand red on hover, not the default focus blue', () => {
|
|
// `ring-brand` is #C8102E. A `/20` wash — what the header icon used — reads
|
|
// as a pale smudge on a white pill rather than as an outline.
|
|
const { container } = setup();
|
|
const cls = container.querySelector('button').className;
|
|
expect(cls).toMatch(/hover:ring-brand(?!\/)/);
|
|
expect(cls).toMatch(/hover:ring-2/);
|
|
});
|
|
|
|
it('outlines the same way for keyboard focus', () => {
|
|
const { container } = setup();
|
|
const cls = container.querySelector('button').className;
|
|
expect(cls).toMatch(/focus-visible:ring-brand(?!\/)/);
|
|
// and never falls back to the user agent's own outline, which is the blue
|
|
// ring this replaces.
|
|
expect(cls).toMatch(/focus-visible:outline-none/);
|
|
});
|
|
|
|
it('floats bottom-right, below the panel layer', () => {
|
|
// The panel is z-index 1200 and its scrim 1300. A launcher above either
|
|
// would sit on top of an open panel.
|
|
const { container } = setup();
|
|
const cls = container.querySelector('button').className;
|
|
expect(cls).toMatch(/fixed/);
|
|
expect(cls).toMatch(/bottom-5/);
|
|
expect(cls).toMatch(/right-5/);
|
|
expect(cls).toMatch(/z-\[1100\]/);
|
|
});
|
|
|
|
it('gets out of the way while the panel is open', () => {
|
|
// The panel docks to the right on a wide screen and covers the page behind
|
|
// a scrim on a narrow one. Either way this button would be underneath it.
|
|
const { container } = render(<AILauncher isOpen onToggle={jest.fn()} />);
|
|
expect(container.querySelector('button')).toBeNull();
|
|
});
|
|
|
|
it('the mark can never outgrow its slot', () => {
|
|
// The header icon hit this twice: a 36px image inside a 32px button,
|
|
// overflowing on every side and breaking the row's alignment.
|
|
const { container } = setup();
|
|
const img = container.querySelector('img');
|
|
expect(img.className).toMatch(/object-contain/);
|
|
expect(img.className).not.toMatch(/max-w-none/);
|
|
expect(img).toHaveAttribute('aria-hidden', 'true');
|
|
});
|
|
});
|