143 lines
6.0 KiB
JavaScript
143 lines
6.0 KiB
JavaScript
import { describe, expect, it } from 'vitest';
|
|
import { checkFormControls, checkMediaWidths } from '../../scripts/check-ui-styles.mjs';
|
|
|
|
const filename = 'src/routes/example/+page.svelte';
|
|
|
|
describe('shared responsive width guard', () => {
|
|
/** @param {string} query */
|
|
const styles = (query) => `<style>\n@media ${query} { .example { display: none; } }\n</style>`;
|
|
|
|
it.each([360, 640, 960, 1100])('allows the shared %i px breakpoint', (width) => {
|
|
for (const feature of ['width', 'min-width', 'max-width']) {
|
|
expect(checkMediaWidths(styles(`(${feature}: ${width}px)`), filename)).toEqual([]);
|
|
}
|
|
});
|
|
|
|
it.each([440, 480, 500, 540, 600, 620, 700, 720, 760, 800, 850, 1000])(
|
|
'rejects the legacy %i px breakpoint in a new component',
|
|
(width) => {
|
|
expect(checkMediaWidths(styles(`(max-width: ${width}px)`), filename)).toEqual([
|
|
`${filename}:2: use a shared media width (360px, 640px, 960px, 1100px): ${width}px`
|
|
]);
|
|
}
|
|
);
|
|
|
|
it('checks every width in compound, comma-separated and range queries', () => {
|
|
expect(
|
|
checkMediaWidths(
|
|
styles('screen and (min-width: 600px) and (max-width: 960px), (width <= 720px)'),
|
|
filename
|
|
)
|
|
).toHaveLength(2);
|
|
expect(checkMediaWidths(styles('(360px < width <= 1100px)'), filename)).toEqual([]);
|
|
expect(checkMediaWidths(styles('(600px<width<=720px)'), filename)).toHaveLength(2);
|
|
expect(checkMediaWidths(styles('(720px >= width)'), filename)).toHaveLength(1);
|
|
expect(checkMediaWidths(styles('(max-width: 40rem)'), filename)).toHaveLength(1);
|
|
});
|
|
|
|
it('limits migration exceptions to the recorded file and widths', () => {
|
|
const legacyFile = 'src/routes/leaderboard/+page.svelte';
|
|
expect(checkMediaWidths(styles('(max-width: 800px)'), legacyFile)).toEqual([]);
|
|
expect(checkMediaWidths(styles('(max-width: 1000px)'), legacyFile)).toEqual([]);
|
|
expect(checkMediaWidths(styles('(max-width: 640px)'), legacyFile)).toEqual([]);
|
|
expect(checkMediaWidths(styles('(max-width: 600px)'), legacyFile)).toHaveLength(1);
|
|
expect(checkMediaWidths(styles('(max-width: 800px)'), filename)).toHaveLength(1);
|
|
});
|
|
|
|
it('checks nested rules and standalone CSS with accurate line numbers', () => {
|
|
const cssFile = 'src/lib/ui/example.css';
|
|
expect(
|
|
checkMediaWidths(
|
|
'@supports (display: grid) {\n@media (max-width: 700px) { a { display: grid; } }\n}',
|
|
cssFile
|
|
)
|
|
).toEqual([`${cssFile}:2: use a shared media width (360px, 640px, 960px, 1100px): 700px`]);
|
|
});
|
|
|
|
it('ignores comments, script strings, declaration values and non-width media features', () => {
|
|
expect(
|
|
checkMediaWidths(
|
|
`<script>const example = '@media (max-width: 700px) {}';</script>
|
|
<!-- @media (max-width: 500px) {} -->
|
|
<style>
|
|
/* @media (max-width: 800px) {} */
|
|
a::before { content: '@media (max-width: 700px) {}'; }
|
|
@media (max-width: /* (max-width: 720px) */ 640px) { a { display: none; } }
|
|
@media (max-height: 700px) and (prefers-reduced-motion: reduce) {
|
|
a { transition: none; }
|
|
}
|
|
</style>`,
|
|
filename
|
|
)
|
|
).toEqual([]);
|
|
expect(checkMediaWidths('<p>No styles</p>', filename)).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('shared form control guard', () => {
|
|
it.each([
|
|
'',
|
|
' type="text"',
|
|
' type="search"',
|
|
' type="password"',
|
|
' type="email"',
|
|
' type="url"',
|
|
' type="tel"',
|
|
' type="number"',
|
|
' type="time"',
|
|
' type={visible ? "text" : "password"}'
|
|
])('rejects raw input%s with a useful location', (attributes) => {
|
|
expect(checkFormControls(`<div>\n<input${attributes} />\n</div>`, filename)).toEqual([
|
|
`${filename}:2: use Field with Input instead of raw <input>`
|
|
]);
|
|
});
|
|
|
|
it('finds selects nested in blocks without confusing event expressions for markup', () => {
|
|
const source =
|
|
'{#if ready}<select onchange={(event) => choose(event.currentTarget.value)}><option>One</option></select>{/if}';
|
|
expect(checkFormControls(source, filename)).toEqual([
|
|
`${filename}:1: use Field with Select instead of raw <select>`
|
|
]);
|
|
});
|
|
|
|
it('allows native controls that do not use the text field presentation', () => {
|
|
const source = [
|
|
'checkbox',
|
|
'radio',
|
|
'file',
|
|
'hidden',
|
|
'range',
|
|
'color',
|
|
'button',
|
|
'submit',
|
|
'reset',
|
|
'image'
|
|
]
|
|
.map((type) => `<input type="${type}" />`)
|
|
.join('\n');
|
|
expect(checkFormControls(source, filename)).toEqual([]);
|
|
expect(checkFormControls('<input type={"checkbox"} />', filename)).toEqual([]);
|
|
});
|
|
|
|
it('ignores comments, script strings and shared components', () => {
|
|
const source =
|
|
'<script>const example = "<input>";</script><!-- <select></select> --><Field label="Search"><Input type="search" /></Field><Select options={[]} />';
|
|
expect(checkFormControls(source, filename)).toEqual([]);
|
|
});
|
|
|
|
it('allows raw controls only in their implementing primitives', () => {
|
|
expect(
|
|
checkFormControls('<input />', 'src/lib/ui/components/primitives/Input.svelte')
|
|
).toEqual([]);
|
|
expect(
|
|
checkFormControls('<select></select>', 'src/lib/ui/components/primitives/Select.svelte')
|
|
).toEqual([]);
|
|
expect(
|
|
checkFormControls('<input />', 'src/lib/ui/components/primitives/Field.svelte')
|
|
).toHaveLength(1);
|
|
expect(checkFormControls('<input />', 'src/lib/ui/components/Input.svelte')).toHaveLength(
|
|
1
|
|
);
|
|
});
|
|
});
|