Commit 74021c8d authored by Mark Florian's avatar Mark Florian

Move security scan status strings to frontend

This moves the status strings from the Rails ConfigurationPresenter to
Vue components in the frontend.

This also updates the pinning tests in the previous commit. It's best to
ignore whitespace changes when viewing this commit, since there are some
indentation changes in the snapshots. For example:

    git show <this commit> --ignore-all-space

Addresses https://gitlab.com/gitlab-org/gitlab/-/issues/323375.
parent b38f54ed
/**
* Return the union of the given components' props options. Required props take
* precendence over non-required props of the same name.
*
* This makes two assumptions:
* - All given components define their props in verbose object format.
* - The components all agree on the `type` of a common prop.
*
* @param {object[]} components The components to derive the union from.
* @returns {object} The union of the props of the given components.
*/
export const propsUnion = (components) =>
components.reduce((acc, component) => {
Object.entries(component.props ?? {}).forEach(([propName, propOptions]) => {
if (process.env.NODE_ENV !== 'production') {
if (typeof propOptions !== 'object' || !('type' in propOptions)) {
throw new Error(
`Cannot create props union: expected verbose prop options for prop "${propName}"`,
);
}
if (propName in acc && acc[propName]?.type !== propOptions?.type) {
throw new Error(
`Cannot create props union: incompatible prop types for prop "${propName}"`,
);
}
}
if (!(propName in acc) || propOptions.required) {
acc[propName] = propOptions;
}
});
return acc;
}, {});
......@@ -88,6 +88,7 @@ export default {
:feature="item"
:gitlab-ci-present="gitlabCiPresent"
:gitlab-ci-history-path="gitlabCiHistoryPath"
:auto-devops-enabled="autoDevopsEnabled"
/>
</template>
......
<script>
import { GlLink } from '@gitlab/ui';
import { propsUnion } from '~/vue_shared/components/lib/utils/props_utils';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST_PROFILES,
} from '~/vue_shared/security_reports/constants';
import StatusDastProfiles from './status_dast_profiles.vue';
import StatusGeneric from './status_generic.vue';
import StatusSast from './status_sast.vue';
const scannerComponentMap = {
[REPORT_TYPE_SAST]: StatusSast,
[REPORT_TYPE_DAST_PROFILES]: StatusDastProfiles,
};
export default {
components: {
GlLink,
},
props: {
feature: {
type: Object,
required: true,
},
gitlabCiPresent: {
type: Boolean,
required: false,
default: false,
},
gitlabCiHistoryPath: {
type: String,
required: false,
default: '',
},
},
inheritAttrs: false,
props: propsUnion([StatusGeneric, ...Object.values(scannerComponentMap)]),
computed: {
canViewCiHistory() {
const { type, configured } = this.feature;
return type === 'sast' && configured && this.gitlabCiPresent;
statusComponent() {
return scannerComponentMap[this.feature.type] ?? StatusGeneric;
},
},
};
</script>
<template>
<div>
{{ feature.status }}
<template v-if="canViewCiHistory">
<br />
<gl-link :href="gitlabCiHistoryPath">{{ s__('SecurityConfiguration|View history') }}</gl-link>
</template>
</div>
<component :is="statusComponent" v-bind="$props" />
</template>
<script>
import { s__ } from '~/locale';
export default {
inheritAttrs: false,
i18n: {
availableForOnDemand: s__('SecurityConfiguration|Available for on-demand DAST'),
},
};
</script>
<template>
<div>{{ $options.i18n.availableForOnDemand }}</div>
</template>
<script>
import { s__ } from '~/locale';
export default {
inheritAttrs: false,
props: {
feature: {
type: Object,
required: true,
},
autoDevopsEnabled: {
type: Boolean,
required: true,
},
},
computed: {
status() {
if (this.feature.configured) {
return this.autoDevopsEnabled
? this.$options.i18n.enabledWithAutoDevOps
: this.$options.i18n.enabled;
}
return this.$options.i18n.notEnabled;
},
},
i18n: {
enabled: s__('SecurityConfiguration|Enabled'),
enabledWithAutoDevOps: s__('SecurityConfiguration|Enabled with Auto DevOps'),
notEnabled: s__('SecurityConfiguration|Not enabled'),
},
};
</script>
<template>
<div>{{ status }}</div>
</template>
<script>
import { GlLink } from '@gitlab/ui';
import StatusGeneric from './status_generic.vue';
export default {
components: {
GlLink,
StatusGeneric,
},
inheritAttrs: false,
props: {
feature: {
type: Object,
required: true,
},
autoDevopsEnabled: {
type: Boolean,
required: true,
},
gitlabCiPresent: {
type: Boolean,
required: false,
default: false,
},
gitlabCiHistoryPath: {
type: String,
required: false,
default: '',
},
},
computed: {
canViewCiHistory() {
return this.feature.configured && this.gitlabCiPresent;
},
},
};
</script>
<template>
<div>
<status-generic :feature="feature" :auto-devops-enabled="autoDevopsEnabled" />
<gl-link v-if="canViewCiHistory" :href="gitlabCiHistoryPath">{{
s__('SecurityConfiguration|View history')
}}</gl-link>
</div>
</template>
......@@ -58,14 +58,11 @@ module Projects
def features
scans = scan_types.map do |scan_type|
if scanner_enabled?(scan_type)
scan(scan_type, configured: true, status: auto_devops_source? ? s_('SecurityConfiguration|Enabled with Auto DevOps') : s_('SecurityConfiguration|Enabled'))
else
scan(scan_type, configured: false, status: s_('SecurityConfiguration|Not enabled'))
end
scan(scan_type, configured: scanner_enabled?(scan_type))
end
dast_profiles_insert(scans)
# DAST On-demand scans is a static (non job) entry. Add it manually.
scans << scan(:dast_profiles, configured: true)
end
def latest_pipeline_path
......@@ -74,23 +71,10 @@ module Projects
project_pipeline_path(self, latest_default_branch_pipeline)
end
# DAST On-demand scans is a static (non job) entry. Add it manually following DAST
# TODO: remove as part of https://gitlab.com/gitlab-org/gitlab/-/issues/323375
def dast_profiles_insert(scans)
index = scans.index { |scan| scan[:type] == :dast }
unless index.nil?
scans.insert(index + 1, scan(:dast_profiles, configured: true, status: s_('SecurityConfiguration|Available for on-demand DAST')))
end
scans
end
def scan(type, configured: false, status:)
def scan(type, configured: false)
{
type: type,
configured: configured,
status: status,
configuration_path: configuration_path(type)
}
end
......
......@@ -39,9 +39,9 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
<table
aria-busy="false"
aria-colcount="3"
aria-describedby="__BVID__76__caption_"
aria-describedby="__BVID__86__caption_"
class="table b-table gl-table b-table-stacked-md"
id="__BVID__76"
id="__BVID__86"
role="table"
>
<!---->
......@@ -129,9 +129,10 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Not enabled
<div>
Not enabled
</div>
<!---->
</div>
</div>
......@@ -205,10 +206,7 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -267,10 +265,7 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Available for on-demand DAST
<!---->
Available for on-demand DAST
</div>
</div>
</td>
......@@ -343,10 +338,7 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -405,10 +397,7 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -467,10 +456,7 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -529,10 +515,7 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -591,10 +574,7 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -653,10 +633,7 @@ exports[`Security Configuration App given no enabled scanners matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -721,9 +698,9 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
<table
aria-busy="false"
aria-colcount="3"
aria-describedby="__BVID__139__caption_"
aria-describedby="__BVID__159__caption_"
class="table b-table gl-table b-table-stacked-md"
id="__BVID__139"
id="__BVID__159"
role="table"
>
<!---->
......@@ -811,9 +788,10 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Not enabled
<div>
Not enabled
</div>
<!---->
</div>
</div>
......@@ -887,10 +865,7 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -949,10 +924,7 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Available for on-demand DAST
<!---->
Available for on-demand DAST
</div>
</div>
</td>
......@@ -1025,10 +997,7 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -1087,10 +1056,7 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Enabled with Auto DevOps
<!---->
Enabled with Auto DevOps
</div>
</div>
</td>
......@@ -1149,10 +1115,7 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Enabled with Auto DevOps
<!---->
Enabled with Auto DevOps
</div>
</div>
</td>
......@@ -1211,10 +1174,7 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -1273,10 +1233,7 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -1349,10 +1306,7 @@ exports[`Security Configuration App given some enabled by ADO matches the snapsh
>
<div>
<div>
Enabled with Auto DevOps
<!---->
Enabled with Auto DevOps
</div>
</div>
</td>
......@@ -1507,10 +1461,9 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Enabled
<br />
<div>
Enabled
</div>
<a
class="gl-link"
......@@ -1590,10 +1543,7 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -1652,10 +1602,7 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Available for on-demand DAST
<!---->
Available for on-demand DAST
</div>
</div>
</td>
......@@ -1728,10 +1675,7 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Enabled
<!---->
Enabled
</div>
</div>
</td>
......@@ -1790,10 +1734,7 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Enabled
<!---->
Enabled
</div>
</div>
</td>
......@@ -1852,10 +1793,7 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Enabled
<!---->
Enabled
</div>
</div>
</td>
......@@ -1914,10 +1852,7 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -1976,10 +1911,7 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Not enabled
<!---->
Not enabled
</div>
</div>
</td>
......@@ -2052,10 +1984,7 @@ exports[`Security Configuration App given some enabled scanners (gitlab-ui) matc
>
<div>
<div>
Enabled
<!---->
Enabled
</div>
</div>
</td>
......
......@@ -65,6 +65,7 @@ describe('ConfigurationTable component', () => {
feature,
gitlabCiPresent: propsData.gitlabCiPresent,
gitlabCiHistoryPath: propsData.gitlabCiHistoryPath,
autoDevopsEnabled: propsData.autoDevopsEnabled,
});
expect(manage.find(ManageFeature).props()).toEqual({
feature,
......
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { pick } from 'lodash';
import FeatureStatus from 'ee/security_configuration/components/feature_status.vue';
import StatusDastProfiles from 'ee/security_configuration/components/status_dast_profiles.vue';
import StatusGeneric from 'ee/security_configuration/components/status_generic.vue';
import StatusSast from 'ee/security_configuration/components/status_sast.vue';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST_PROFILES,
} from '~/vue_shared/security_reports/constants';
import { generateFeatures } from './helpers';
const gitlabCiHistoryPath = '/ci/history';
const props = {
gitlabCiPresent: true,
gitlabCiHistoryPath: '/ci-history',
autoDevopsEnabled: false,
};
const attrs = {
'data-foo': 'bar',
};
describe('FeatureStatus component', () => {
let wrapper;
let feature;
const createComponent = (options) => {
wrapper = shallowMount(FeatureStatus, options);
......@@ -15,39 +29,37 @@ describe('FeatureStatus component', () => {
afterEach(() => {
wrapper.destroy();
feature = undefined;
});
const findHistoryLink = () => wrapper.find(GlLink);
describe.each`
context | type | configured | gitlabCiPresent | shouldShowHistory
${'no CI with sast disabled'} | ${'sast'} | ${false} | ${false} | ${false}
${'CI with sast disabled'} | ${'sast'} | ${false} | ${true} | ${false}
${'no CI with sast enabled'} | ${'sast'} | ${true} | ${false} | ${false}
${'CI with foo enabled'} | ${'foo'} | ${true} | ${true} | ${false}
${'CI with sast enabled'} | ${'sast'} | ${true} | ${true} | ${true}
`('given $context', ({ type, configured, gitlabCiPresent, shouldShowHistory }) => {
type | expectedComponent
${REPORT_TYPE_SAST} | ${StatusSast}
${REPORT_TYPE_DAST_PROFILES} | ${StatusDastProfiles}
${'foo'} | ${StatusGeneric}
`('given a $type feature', ({ type, expectedComponent }) => {
let feature;
let component;
beforeEach(() => {
[feature] = generateFeatures(1, { type, configured });
[feature] = generateFeatures(1, { type });
createComponent({
propsData: { feature, gitlabCiPresent, gitlabCiHistoryPath },
propsData: { feature, ...props },
attrs,
});
});
it('shows feature status text', () => {
expect(wrapper.text()).toContain(feature.status);
component = wrapper.findComponent(expectedComponent);
});
it(`${shouldShowHistory ? 'shows' : 'does not show'} the history link`, () => {
expect(findHistoryLink().exists()).toBe(shouldShowHistory);
it('renders expected component', () => {
expect(component.exists()).toBe(true);
});
if (shouldShowHistory) {
it("sets the link's href correctly", () => {
expect(findHistoryLink().attributes('href')).toBe(gitlabCiHistoryPath);
});
}
it('passes through props to expected component', () => {
// Exclude props not defined on the expected component, since
// @vue/test-utils won't include them in `Wrapper#props`.
const expectedProps = pick({ feature, ...props }, Object.keys(expectedComponent.props ?? {}));
expect(component.props()).toEqual(expectedProps);
});
});
});
import { shallowMount } from '@vue/test-utils';
import StatusDastProfiles from 'ee/security_configuration/components/status_dast_profiles.vue';
describe('StatusDastProfiles component', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(StatusDastProfiles);
};
afterEach(() => {
wrapper.destroy();
});
it('renders the fixed DAST Profiles status', () => {
createComponent();
expect(wrapper.element).toMatchInlineSnapshot(`
<div>
Available for on-demand DAST
</div>
`);
});
});
import { shallowMount } from '@vue/test-utils';
import StatusGeneric from 'ee/security_configuration/components/status_generic.vue';
import { generateFeatures } from './helpers';
describe('StatusGeneric component', () => {
let wrapper;
const createComponent = (options) => {
wrapper = shallowMount(StatusGeneric, options);
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
context | configured | autoDevopsEnabled | status
${'not configured'} | ${false} | ${false} | ${StatusGeneric.i18n.notEnabled}
${'not configured, but Auto DevOps is enabled'} | ${false} | ${true} | ${StatusGeneric.i18n.notEnabled}
${'configured'} | ${true} | ${false} | ${StatusGeneric.i18n.enabled}
${'configured with Auto DevOps'} | ${true} | ${true} | ${StatusGeneric.i18n.enabledWithAutoDevOps}
`('given the feature is $context', ({ configured, autoDevopsEnabled, status }) => {
let feature;
beforeEach(() => {
[feature] = generateFeatures(1, { configured });
createComponent({
propsData: { feature, autoDevopsEnabled },
});
});
it(`shows the status "${status}"`, () => {
expect(wrapper.text()).toBe(status);
});
});
});
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import StatusGeneric from 'ee/security_configuration/components/status_generic.vue';
import StatusSast from 'ee/security_configuration/components/status_sast.vue';
import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { generateFeatures } from './helpers';
const gitlabCiHistoryPath = '/ci/history';
const autoDevopsEnabled = true;
describe('StatusSast component', () => {
let wrapper;
const createComponent = (options) => {
wrapper = shallowMount(StatusSast, options);
};
afterEach(() => {
wrapper.destroy();
});
const findHistoryLink = () => wrapper.find(GlLink);
describe.each`
context | configured | gitlabCiPresent | shouldShowHistory
${'no CI with sast disabled'} | ${false} | ${false} | ${false}
${'CI with sast disabled'} | ${false} | ${true} | ${false}
${'no CI with sast enabled'} | ${true} | ${false} | ${false}
${'CI with sast enabled'} | ${true} | ${true} | ${true}
`('given $context', ({ configured, gitlabCiPresent, shouldShowHistory }) => {
let feature;
beforeEach(() => {
[feature] = generateFeatures(1, { type: REPORT_TYPE_SAST, configured });
createComponent({
propsData: { feature, gitlabCiPresent, gitlabCiHistoryPath, autoDevopsEnabled },
});
});
it('shows the generic status', () => {
const genericComponent = wrapper.findComponent(StatusGeneric);
expect(genericComponent.exists()).toBe(true);
expect(genericComponent.props()).toEqual({
feature,
autoDevopsEnabled,
});
});
it(`${shouldShowHistory ? 'shows' : 'does not show'} the history link`, () => {
expect(findHistoryLink().exists()).toBe(shouldShowHistory);
});
if (shouldShowHistory) {
it("sets the link's href correctly", () => {
expect(findHistoryLink().attributes('href')).toBe(gitlabCiHistoryPath);
});
}
});
});
......@@ -80,15 +80,15 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
it 'reports that all scanners are configured for which latest pipeline has builds' do
expect(Gitlab::Json.parse(subject[:features])).to contain_exactly(
security_scan(:dast, configured: true, auto_dev_ops_enabled: true),
security_scan(:dast_profiles, configured: true, auto_dev_ops_enabled: true),
security_scan(:sast, configured: true, auto_dev_ops_enabled: true),
security_scan(:container_scanning, configured: false, auto_dev_ops_enabled: true),
security_scan(:dependency_scanning, configured: false, auto_dev_ops_enabled: true),
security_scan(:license_scanning, configured: false, auto_dev_ops_enabled: true),
security_scan(:secret_detection, configured: true, auto_dev_ops_enabled: true),
security_scan(:coverage_fuzzing, configured: false, auto_dev_ops_enabled: true),
security_scan(:api_fuzzing, configured: false, auto_dev_ops_enabled: true)
security_scan(:dast, configured: true),
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false),
security_scan(:license_scanning, configured: false),
security_scan(:secret_detection, configured: true),
security_scan(:coverage_fuzzing, configured: false),
security_scan(:api_fuzzing, configured: false),
security_scan(:dast_profiles, configured: true)
)
end
end
......@@ -105,14 +105,14 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
it 'reports all security jobs as unconfigured' do
expect(Gitlab::Json.parse(subject[:features])).to contain_exactly(
security_scan(:dast, configured: false),
security_scan(:dast_profiles, configured: true),
security_scan(:sast, configured: false),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false),
security_scan(:license_scanning, configured: false),
security_scan(:secret_detection, configured: false),
security_scan(:coverage_fuzzing, configured: false),
security_scan(:api_fuzzing, configured: false)
security_scan(:api_fuzzing, configured: false),
security_scan(:dast_profiles, configured: true)
)
end
end
......@@ -254,15 +254,12 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
end
end
def security_scan(type, configured:, auto_dev_ops_enabled: false)
def security_scan(type, configured:)
configuration_path = configuration_path(type)
status_str = scan_status(type, configured, auto_dev_ops_enabled)
{
"type" => type.to_s,
"configured" => configured,
"status" => status_str,
"configuration_path" => configuration_path
}
end
......@@ -274,16 +271,4 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
api_fuzzing: project_security_configuration_api_fuzzing_path(project)
}[type]
end
def scan_status(type, configured, auto_dev_ops_enabled)
if type == :dast_profiles
"Available for on-demand DAST"
elsif configured && auto_dev_ops_enabled
"Enabled with Auto DevOps"
elsif configured
"Enabled"
else
"Not enabled"
end
end
end
import { propsUnion } from '~/vue_shared/components/lib/utils/props_utils';
describe('propsUnion', () => {
const stringRequired = {
type: String,
required: true,
};
const stringOptional = {
type: String,
required: false,
};
const numberOptional = {
type: Number,
required: false,
};
const booleanRequired = {
type: Boolean,
required: true,
};
const FooComponent = {
props: { foo: stringRequired },
};
const BarComponent = {
props: { bar: numberOptional },
};
const FooBarComponent = {
props: {
foo: stringRequired,
bar: numberOptional,
},
};
const FooOptionalComponent = {
props: {
foo: stringOptional,
},
};
const QuxComponent = {
props: {
foo: booleanRequired,
qux: stringRequired,
},
};
it('returns an empty object given no components', () => {
expect(propsUnion([])).toEqual({});
});
it('merges non-overlapping props', () => {
expect(propsUnion([FooComponent, BarComponent])).toEqual({
...FooComponent.props,
...BarComponent.props,
});
});
it('merges overlapping props', () => {
expect(propsUnion([FooComponent, BarComponent, FooBarComponent])).toEqual({
...FooComponent.props,
...BarComponent.props,
...FooBarComponent.props,
});
});
it.each`
components
${[FooComponent, FooOptionalComponent]}
${[FooOptionalComponent, FooComponent]}
`('prefers required props over non-required props', ({ components }) => {
expect(propsUnion(components)).toEqual(FooComponent.props);
});
it('throws if given props with conflicting types', () => {
expect(() => propsUnion([FooComponent, QuxComponent])).toThrow(/incompatible prop types/);
});
it.each`
components
${[{ props: ['foo', 'bar'] }]}
${[{ props: { foo: String, bar: Number } }]}
${[{ props: { foo: {}, bar: {} } }]}
`('throw if given a non-verbose props object', ({ components }) => {
expect(() => propsUnion(components)).toThrow(/expected verbose prop/);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment