Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated parameterized test pattern #1379

Merged
merged 14 commits into from
Sep 19, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
rajsite marked this conversation as resolved.
Show resolved Hide resolved
"type": "none",
"comment": "Parameterize test pattern",
"packageName": "@ni/nimble-components",
"email": "rajsite@users.noreply.github.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { html } from '@microsoft/fast-element';
import { AnchorButton } from '..';
import { waitForUpdatesAsync } from '../../testing/async-helpers';
import { fixture, Fixture } from '../../utilities/tests/fixture';
import { getSpecTypeByNamedList } from '../../utilities/tests/parameterized';
import { parameterizeList } from '../../utilities/tests/parameterized';

async function setup(): Promise<Fixture<AnchorButton>> {
return fixture<AnchorButton>(
Expand Down Expand Up @@ -49,55 +49,45 @@ describe('AnchorButton', () => {
expect(element.control!.getAttribute('href')).toBeNull();
});

const attributeNames: { name: string }[] = [
{ name: 'download' },
{ name: 'hreflang' },
{ name: 'ping' },
{ name: 'referrerpolicy' },
{ name: 'rel' },
{ name: 'target' },
{ name: 'type' },
{ name: 'aria-atomic' },
{ name: 'aria-busy' },
{ name: 'aria-controls' },
{ name: 'aria-current' },
{ name: 'aria-describedby' },
{ name: 'aria-details' },
{ name: 'aria-disabled' },
{ name: 'aria-errormessage' },
{ name: 'aria-expanded' },
{ name: 'aria-flowto' },
{ name: 'aria-haspopup' },
{ name: 'aria-hidden' },
{ name: 'aria-invalid' },
{ name: 'aria-keyshortcuts' },
{ name: 'aria-label' },
{ name: 'aria-labelledby' },
{ name: 'aria-live' },
{ name: 'aria-owns' },
{ name: 'aria-relevant' },
{ name: 'aria-roledescription' }
];
const attributeNames = [
'download',
'hreflang',
'ping',
'referrerpolicy',
'rel',
'target',
'type',
'aria-atomic',
'aria-busy',
'aria-controls',
'aria-current',
'aria-describedby',
'aria-details',
'aria-disabled',
'aria-errormessage',
'aria-expanded',
'aria-flowto',
'aria-haspopup',
'aria-hidden',
'aria-invalid',
'aria-keyshortcuts',
'aria-label',
'aria-labelledby',
'aria-live',
'aria-owns',
'aria-relevant',
'aria-roledescription'
] as const;
describe('should reflect value to the internal control', () => {
const focused: string[] = [];
const disabled: string[] = [];
for (const attribute of attributeNames) {
const specType = getSpecTypeByNamedList(
attribute,
focused,
disabled
);
// eslint-disable-next-line @typescript-eslint/no-loop-func
specType(`for attribute ${attribute.name}`, async () => {
parameterizeList(attributeNames, (spec, name) => {
spec(`for attribute ${name}`, async () => {
await connect();

element.setAttribute(attribute.name, 'foo');
element.setAttribute(name, 'foo');
await waitForUpdatesAsync();

expect(element.control!.getAttribute(attribute.name)).toBe(
'foo'
);
expect(element.control!.getAttribute(name)).toBe('foo');
});
}
});
});
});
152 changes: 116 additions & 36 deletions packages/nimble-components/src/utilities/tests/parameterized.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
// eslint-disable-next-line no-restricted-globals
rajsite marked this conversation as resolved.
Show resolved Hide resolved
type SpecTypes = typeof fit | typeof xit | typeof it;
/**
* Note: you should probably use one of the specialized versions of this function instead
*
* Used to focus or disable specific tests in a parameterized test run.
* In the following example the test for the `cats` case is focused for debugging
* and no tests are disabled:
* @example
* const rainTypes = ['cats', 'dogs', 'frogs', 'men'];
* describe('Different rains', () => {
* const isFocused = rainType => rainType === 'cats';
* const isDisabled = () => false;
* for (const rainType of rainTypes) {
* const specType = getSpecType(rainType, isFocused, isDisabled);
* specType(`of type ${rainType} exist`, () => {
* expect(rainType).toBeTruthy();
* });
* }
* });
* @deprecated switch to `parameterize` or `parameterizeList` instead
*/
const getSpecType = <T>(
value: T,
Expand All @@ -35,25 +19,7 @@ const getSpecType = <T>(
};

/**
* Used to focus or disable specific tests in a parameterized test run using a list of named objects.
* In the following example the test for the `cats-and-dogs` case is focused for debugging
* and no tests are disabled:
* @example
* const rainTypes = [
* { name: 'cats-and-dogs', type: 'idiom' },
* { name: 'frogs' type: 'idiom'},
* { name: 'men', type: 'lyrics'}
* ] as const;
* describe('Different rains', () => {
* const focused = ['cats-and-dogs'];
* const disabled = [];
* for (const rainType of rainTypes) {
* const specType = getSpecTypeByNamedList(rainType, focused, disabled);
* specType(`of type ${rainType.name} exist`, () => {
* expect(rainType.type).toBeDefined();
* });
* }
* });
* @deprecated switch to `parameterize` or `parameterizeList` instead
*/
export const getSpecTypeByNamedList = <T extends { name: string }>(
value: T,
Expand All @@ -64,3 +30,117 @@ export const getSpecTypeByNamedList = <T extends { name: string }>(
(x: T) => focusList.includes(x.name),
(x: T) => disabledList.includes(x.name)
);

// eslint-disable-next-line no-restricted-globals
rajsite marked this conversation as resolved.
Show resolved Hide resolved
type Fit = typeof fit;
type Xit = typeof xit;
type It = typeof it;
/**
* One of the jasmine spec functions: fit, xit, or it
*/
type Spec = Fit | Xit | It;
/**
* One of the jasmine spec functions: fit or xit
*/
type SpecOverride = Fit | Xit;

/**
* Used to create a parameterized test using an object of test names and arbitrary test values.
* In the following example:
* - the test named `catsAndDogs` is focused for debugging
* - the test named `frogs` is configured to always be disabled
* - the test named `men` will run normally as it has no override
* @example
* const rainTests = {
* catsAndDogs: 'idiom',
rajsite marked this conversation as resolved.
Show resolved Hide resolved
* frogs: 'idiom',
* men: 'lyrics'
* } as const;
* describe('Different rains', () => {
* parameterize(rainTests, (spec, name, value) => {
* spec(`of type ${name} exist`, () => {
* expect(value).toBeDefined();
* });
* }, {
* catsAndDogs: fit,
* frogs: xit
* });
* });
*/
export const parameterize = <T extends object>(
testCases: T,
test: (spec: Spec, name: keyof T, value: T[keyof T]) => void,
specOverrides?: {
[P in keyof T]?: SpecOverride;
}
): void => {
const testCaseNames = Object.keys(testCases) as (keyof T)[];
if (specOverrides) {
const overrideNames = Object.keys(
specOverrides
) as (keyof typeof specOverrides)[];
if (
!overrideNames.every(overrideName => testCaseNames.includes(overrideName))
) {
throw new Error(
'Parameterized test override names must match test case name'
);
}
if (
// eslint-disable-next-line no-restricted-globals
!overrideNames.every(overrideName => [fit, xit].includes(specOverrides[overrideName]!))
) {
throw new Error(
'Must configure override with one of the jasmine spec functions: fit or xit'
);
}
}
testCaseNames.forEach(testCaseName => {
const spec = specOverrides?.[testCaseName] ?? it;
test(spec, testCaseName, testCases[testCaseName]);
});
};

type ObjectFromList<T extends readonly string[]> = {
[K in T extends readonly (infer U)[] ? U : never]: K;
};

/**
* Used to create a parameterized test using an array of test names.
* In the following example:
* - the test named `cats-and-dogs` is focused for debugging
* - the test named `frogs` is configured to always be disabled
* - the test named `men` will run normally as it has no override
* @example
* const rainTests = [
* 'cats-and-dogs',
* 'frogs',
* 'men'
* ] as const;
* describe('Different rains', () => {
* parameterizeList(rainTests, (spec, name) => {
* spec(`of type ${name} exist`, () => {
* expect(name).toBeDefined();
* });
* }, {
* 'cats-and-dogs': fit,
* frogs: xit
* });
* });
*/
export const parameterizeList = <T extends readonly string[]>(
list: T,
test: (spec: Spec, name: keyof ObjectFromList<T>) => void,
specOverrides?: {
[P in keyof ObjectFromList<T>]?: SpecOverride;
}
): void => {
const testCases = list.reduce<{ [key: string]: string }>(
(result, entry) => {
result[entry] = entry;
return result;
},
{}
) as ObjectFromList<T>;
parameterize(testCases, test, specOverrides);
};
Loading