updates on bugs on the dispatch and ai button integrations
This commit is contained in:
87
tests/ds/aiLauncher.test.jsx
Normal file
87
tests/ds/aiLauncher.test.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
70
tests/ds/dispatchHeaderClipping.test.js
Normal file
70
tests/ds/dispatchHeaderClipping.test.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* The Control X header must not clip its own dropdowns.
|
||||
*
|
||||
* `.logo` holds the page title, the hub picker and the batch picker. Both
|
||||
* pickers render their menu as an absolutely-positioned child of a
|
||||
* `position: relative` wrapper that sits INSIDE `.logo`, so `.logo` is what
|
||||
* decides whether the menu is drawn at all. `#hdr` is 56px tall and the menus
|
||||
* open below their trigger, so with `overflow: hidden` on `.logo` every pixel
|
||||
* of them fell outside the box: the trigger highlighted, the caret rotated, and
|
||||
* nothing appeared. The batch menu's `z-index: 9999` could not save it — z-index
|
||||
* does not escape a clipping ancestor.
|
||||
*
|
||||
* Asserted against the stylesheet because this cannot be caught in jsdom, which
|
||||
* does not lay out or clip anything, and the page it belongs to is a
|
||||
* ~5,800-line Leaflet component that no unit test mounts.
|
||||
*/
|
||||
const css = fs.readFileSync(
|
||||
path.join(__dirname, '../../src/pages/doormile/dispatch/Dispatch.css'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
/** The declarations inside one selector's first rule block. */
|
||||
const ruleFor = (selector) => {
|
||||
const at = css.indexOf(selector + ' {');
|
||||
if (at === -1) throw new Error(`no rule for ${selector}`);
|
||||
return css.slice(at, css.indexOf('}', at));
|
||||
};
|
||||
|
||||
const overflowIn = (block) => {
|
||||
const m = [...block.matchAll(/(?:^|\n)\s*overflow\s*:\s*([^;]+);/g)];
|
||||
return m.length ? m[m.length - 1][1].trim() : null;
|
||||
};
|
||||
|
||||
describe('the Control X header', () => {
|
||||
it('does not clip the group that contains the dropdowns', () => {
|
||||
expect(overflowIn(ruleFor('.dispatch-container .logo'))).not.toBe('hidden');
|
||||
});
|
||||
|
||||
it('does not clip the tab group, which holds the batch menu', () => {
|
||||
expect(overflowIn(ruleFor('.dispatch-container #dispatch-top-tabs'))).not.toBe('hidden');
|
||||
});
|
||||
|
||||
it('still keeps the group shrinkable, which is what min-width does', () => {
|
||||
// Removing the overflow must not bring back the overflowing header row
|
||||
// that pushed the clock past the right gutter. `min-width: 0` is the rule
|
||||
// that actually lets a flex item shrink below its content width.
|
||||
expect(ruleFor('.dispatch-container .logo')).toMatch(/min-width:\s*0/);
|
||||
});
|
||||
|
||||
it('clips the title instead, so a narrow header still cannot spill', () => {
|
||||
const title = ruleFor('.dispatch-container .logo-name');
|
||||
expect(title).toMatch(/overflow:\s*hidden/);
|
||||
expect(title).toMatch(/text-overflow:\s*ellipsis/);
|
||||
});
|
||||
|
||||
it('leaves the badge clipping its own transparent padding', () => {
|
||||
// .logo-badge scales its image 150% to cancel the PNG's padding and relies
|
||||
// on its OWN overflow to crop it. That is unrelated to the dropdowns.
|
||||
expect(ruleFor('.dispatch-container .logo-badge')).toMatch(/overflow:\s*hidden/);
|
||||
});
|
||||
|
||||
it('the menu is still positioned under its trigger', () => {
|
||||
const menu = ruleFor('.dispatch-container .logo-city-menu');
|
||||
expect(menu).toMatch(/position:\s*absolute/);
|
||||
expect(menu).toMatch(/top:\s*calc\(100% \+ 6px\)/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user