Skip to content

Commit e5c9de8

Browse files
committed
feat(theme-check-common): add ValidSelectDefault check
Warns when a select/radio setting's default value, a preset's setting value, or a section default.settings value is not one of the setting's declared options. Covers {% schema %} in sections/blocks and config/settings_schema.json. Closes #943
1 parent ac5393c commit e5c9de8

4 files changed

Lines changed: 582 additions & 0 deletions

File tree

.changeset/valid-select-default.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@shopify/theme-check-common": minor
3+
---
4+
5+
Add `ValidSelectDefault` check that warns when a `select` or `radio` setting's `default` value is not one of its declared `options`. The check also validates preset setting values and section `default.settings` values, and runs against `config/settings_schema.json`.

packages/theme-check-common/src/checks/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ import { ValidRenderSnippetArgumentTypes } from './valid-render-snippet-argument
6161
import { ValidSchema } from './valid-schema';
6262
import { ValidSchemaName } from './valid-schema-name';
6363
import { ValidSchemaTranslations } from './valid-schema-translations';
64+
import {
65+
ValidSelectDefault,
66+
ValidSelectDefaultSettingsSchema,
67+
} from './valid-select-default';
6468
import { ValidSettingsKey } from './valid-settings-key';
6569
import { ValidStaticBlockType } from './valid-static-block-type';
6670
import { ValidVisibleIf, ValidVisibleIfSettingsSchema } from './valid-visible-if';
@@ -130,6 +134,8 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [
130134
ValidLocalBlocks,
131135
ValidRenderSnippetArgumentTypes,
132136
ValidSchema,
137+
ValidSelectDefault,
138+
ValidSelectDefaultSettingsSchema,
133139
ValidSettingsKey,
134140
ValidStaticBlockType,
135141
ValidVisibleIf,
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { check, runJSONCheck } from '../../test';
3+
import { ValidSelectDefault, ValidSelectDefaultSettingsSchema } from './index';
4+
5+
function toLiquidFile(content: unknown) {
6+
return `
7+
{% schema %}
8+
${JSON.stringify(content)}
9+
{% endschema %}
10+
`;
11+
}
12+
13+
const alignmentOptions = [
14+
{ value: 'flex-start', label: 'Left' },
15+
{ value: 'center', label: 'Center' },
16+
{ value: 'flex-end', label: 'Right' },
17+
];
18+
19+
describe('Module: ValidSelectDefault (Liquid schema)', () => {
20+
describe('setting defaults', () => {
21+
it('does not report when the select default matches an option', async () => {
22+
const theme = {
23+
'sections/example.liquid': toLiquidFile({
24+
name: 'Example',
25+
settings: [
26+
{
27+
type: 'select',
28+
id: 'alignment',
29+
default: 'center',
30+
options: alignmentOptions,
31+
},
32+
],
33+
}),
34+
};
35+
36+
const offenses = await check(theme, [ValidSelectDefault]);
37+
expect(offenses).toHaveLength(0);
38+
});
39+
40+
it('reports when the select default is not in options', async () => {
41+
const theme = {
42+
'sections/example.liquid': toLiquidFile({
43+
name: 'Example',
44+
settings: [
45+
{
46+
type: 'select',
47+
id: 'alignment',
48+
default: 'this-is-not-an-option',
49+
options: alignmentOptions,
50+
},
51+
],
52+
}),
53+
};
54+
55+
const offenses = await check(theme, [ValidSelectDefault]);
56+
expect(offenses).toHaveLength(1);
57+
expect(offenses[0].message).toMatch(/alignment/);
58+
expect(offenses[0].message).toMatch(/this-is-not-an-option/);
59+
expect(offenses[0].message).toMatch(/flex-start/);
60+
});
61+
62+
it('reports when a radio default is not in options', async () => {
63+
const theme = {
64+
'sections/example.liquid': toLiquidFile({
65+
name: 'Example',
66+
settings: [
67+
{
68+
type: 'radio',
69+
id: 'layout_direction',
70+
default: 'bogus',
71+
options: [
72+
{ value: 'row', label: 'Row' },
73+
{ value: 'column', label: 'Column' },
74+
],
75+
},
76+
],
77+
}),
78+
};
79+
80+
const offenses = await check(theme, [ValidSelectDefault]);
81+
expect(offenses).toHaveLength(1);
82+
expect(offenses[0].message).toMatch(/layout_direction/);
83+
expect(offenses[0].message).toMatch(/bogus/);
84+
});
85+
86+
it('does not report when the setting has no default', async () => {
87+
const theme = {
88+
'sections/example.liquid': toLiquidFile({
89+
name: 'Example',
90+
settings: [
91+
{
92+
type: 'select',
93+
id: 'alignment',
94+
options: alignmentOptions,
95+
},
96+
],
97+
}),
98+
};
99+
100+
const offenses = await check(theme, [ValidSelectDefault]);
101+
expect(offenses).toHaveLength(0);
102+
});
103+
104+
it('ignores non-choice settings', async () => {
105+
const theme = {
106+
'sections/example.liquid': toLiquidFile({
107+
name: 'Example',
108+
settings: [
109+
{ type: 'text', id: 'heading', default: 'anything' },
110+
],
111+
}),
112+
};
113+
114+
const offenses = await check(theme, [ValidSelectDefault]);
115+
expect(offenses).toHaveLength(0);
116+
});
117+
});
118+
119+
describe('preset settings', () => {
120+
it('does not report when preset setting matches an option', async () => {
121+
const theme = {
122+
'sections/example.liquid': toLiquidFile({
123+
name: 'Example',
124+
settings: [
125+
{
126+
type: 'select',
127+
id: 'alignment',
128+
options: alignmentOptions,
129+
},
130+
],
131+
presets: [
132+
{
133+
name: 'Column',
134+
settings: { alignment: 'flex-end' },
135+
},
136+
],
137+
}),
138+
};
139+
140+
const offenses = await check(theme, [ValidSelectDefault]);
141+
expect(offenses).toHaveLength(0);
142+
});
143+
144+
it('reports when preset setting is not in options', async () => {
145+
const theme = {
146+
'sections/example.liquid': toLiquidFile({
147+
name: 'Example',
148+
settings: [
149+
{
150+
type: 'select',
151+
id: 'alignment',
152+
options: alignmentOptions,
153+
},
154+
],
155+
presets: [
156+
{
157+
name: 'Column',
158+
settings: { alignment: 'this-is-not-an-option' },
159+
},
160+
],
161+
}),
162+
};
163+
164+
const offenses = await check(theme, [ValidSelectDefault]);
165+
expect(offenses).toHaveLength(1);
166+
expect(offenses[0].message).toMatch(/alignment/);
167+
expect(offenses[0].message).toMatch(/this-is-not-an-option/);
168+
});
169+
170+
it('reports both invalid default and invalid preset setting', async () => {
171+
const theme = {
172+
'sections/example.liquid': toLiquidFile({
173+
name: 'Example',
174+
settings: [
175+
{
176+
type: 'select',
177+
id: 'alignment',
178+
default: 'bad-default',
179+
options: alignmentOptions,
180+
},
181+
],
182+
presets: [
183+
{
184+
name: 'Column',
185+
settings: { alignment: 'also-bad' },
186+
},
187+
],
188+
}),
189+
};
190+
191+
const offenses = await check(theme, [ValidSelectDefault]);
192+
expect(offenses).toHaveLength(2);
193+
});
194+
195+
it('does not report when preset references a non-select setting', async () => {
196+
const theme = {
197+
'sections/example.liquid': toLiquidFile({
198+
name: 'Example',
199+
settings: [{ type: 'text', id: 'heading' }],
200+
presets: [
201+
{
202+
name: 'Column',
203+
settings: { heading: 'free-form text' },
204+
},
205+
],
206+
}),
207+
};
208+
209+
const offenses = await check(theme, [ValidSelectDefault]);
210+
expect(offenses).toHaveLength(0);
211+
});
212+
213+
it('reports invalid values in section default.settings', async () => {
214+
const theme = {
215+
'sections/example.liquid': toLiquidFile({
216+
name: 'Example',
217+
settings: [
218+
{
219+
type: 'select',
220+
id: 'alignment',
221+
options: alignmentOptions,
222+
},
223+
],
224+
default: {
225+
settings: { alignment: 'nope' },
226+
},
227+
}),
228+
};
229+
230+
const offenses = await check(theme, [ValidSelectDefault]);
231+
expect(offenses).toHaveLength(1);
232+
expect(offenses[0].message).toMatch(/alignment/);
233+
expect(offenses[0].message).toMatch(/nope/);
234+
});
235+
});
236+
});
237+
238+
describe('Module: ValidSelectDefaultSettingsSchema (config/settings_schema.json)', () => {
239+
it('reports an invalid default in config/settings_schema.json', async () => {
240+
const source = JSON.stringify([
241+
{
242+
name: 'Layout',
243+
settings: [
244+
{
245+
type: 'select',
246+
id: 'alignment',
247+
default: 'this-is-not-an-option',
248+
options: alignmentOptions,
249+
},
250+
],
251+
},
252+
]);
253+
254+
const offenses = await runJSONCheck(
255+
ValidSelectDefaultSettingsSchema,
256+
source,
257+
'config/settings_schema.json',
258+
);
259+
expect(offenses).toHaveLength(1);
260+
expect(offenses[0].message).toMatch(/alignment/);
261+
expect(offenses[0].message).toMatch(/this-is-not-an-option/);
262+
});
263+
264+
it('does not report a valid default', async () => {
265+
const source = JSON.stringify([
266+
{
267+
name: 'Layout',
268+
settings: [
269+
{
270+
type: 'select',
271+
id: 'alignment',
272+
default: 'center',
273+
options: alignmentOptions,
274+
},
275+
],
276+
},
277+
]);
278+
279+
const offenses = await runJSONCheck(
280+
ValidSelectDefaultSettingsSchema,
281+
source,
282+
'config/settings_schema.json',
283+
);
284+
expect(offenses).toHaveLength(0);
285+
});
286+
287+
it('does not run on files other than settings_schema.json', async () => {
288+
const source = JSON.stringify([
289+
{
290+
name: 'Layout',
291+
settings: [
292+
{
293+
type: 'select',
294+
id: 'alignment',
295+
default: 'this-is-not-an-option',
296+
options: alignmentOptions,
297+
},
298+
],
299+
},
300+
]);
301+
302+
const offenses = await runJSONCheck(
303+
ValidSelectDefaultSettingsSchema,
304+
source,
305+
'config/other.json',
306+
);
307+
expect(offenses).toHaveLength(0);
308+
});
309+
});

0 commit comments

Comments
 (0)