diff --git a/packages/combobox/src/Combobox/Combobox.spec.tsx b/packages/combobox/src/Combobox/Combobox.spec.tsx
index cd3f66b275..396371e96d 100644
--- a/packages/combobox/src/Combobox/Combobox.spec.tsx
+++ b/packages/combobox/src/Combobox/Combobox.spec.tsx
@@ -29,6 +29,8 @@ import {
testif,
} from '../utils/ComboboxTestUtils';
+import { Combobox } from './Combobox';
+
/**
* Tests
*/
@@ -70,6 +72,8 @@ describe('packages/combobox', () => {
: testif(select === 'multiple')(name, fn);
describe('Basic rendering', () => {
+ test('spreads rest', () => expect(Combobox).toSpreadRest());
+
// Label prop
test('Label is rendered', () => {
const { labelEl } = renderCombobox(select, { label: 'Some label' });
diff --git a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx
index 9c49d7485f..efecdcd64e 100644
--- a/packages/date-picker/src/DatePicker/DatePicker.spec.tsx
+++ b/packages/date-picker/src/DatePicker/DatePicker.spec.tsx
@@ -71,6 +71,8 @@ describe('packages/date-picker', () => {
/// Note: Many rendering tests should be handled by Chromatic
describe('Input', () => {
+ test('spreads rest', () => expect(DatePicker).toSpreadRest());
+
test('renders label', () => {
const { getByText } = render();
const label = getByText('Label');
diff --git a/packages/text-input/src/TextInput/TextInput.spec.tsx b/packages/text-input/src/TextInput/TextInput.spec.tsx
index bc50fa648c..4be380094e 100644
--- a/packages/text-input/src/TextInput/TextInput.spec.tsx
+++ b/packages/text-input/src/TextInput/TextInput.spec.tsx
@@ -47,12 +47,27 @@ describe('packages/text-input', () => {
expect(results).toHaveNoViolations();
});
});
+
test(`renders ${defaultProps.label} as the input label and ${defaultProps.description} as the description`, () => {
const { label, description } = renderTextInput(defaultProps);
expect(label?.innerHTML).toContain(defaultProps.label);
expect(description?.innerHTML).toContain(defaultProps.description);
});
+ test('allows external label', () => {
+ const { getByTestId } = render(
+ <>
+
+
+ >,
+ );
+
+ const input = getByTestId('input');
+ expect(input).toBeLabelled();
+ });
+
test(`renders ${defaultProps.placeholder} as placeholder text`, () => {
const { getByPlaceholderText } = renderTextInput(defaultProps);
expect(getByPlaceholderText(defaultProps.placeholder)).toBeVisible();
diff --git a/tools/jest-matchers/README.md b/tools/jest-matchers/README.md
new file mode 100644
index 0000000000..7fb8e93d15
--- /dev/null
+++ b/tools/jest-matchers/README.md
@@ -0,0 +1,19 @@
+# Jest Matchers
+
+
+
+#### [View on MongoDB.design](https://www.mongodb.design/component/jest-matchers/example/)
+
+## Installation
+
+### Yarn
+
+```shell
+yarn add @leafygreen-ui/jest-matchers
+```
+
+### NPM
+
+```shell
+npm install @leafygreen-ui/jest-matchers
+```
diff --git a/tools/jest-matchers/package.json b/tools/jest-matchers/package.json
new file mode 100644
index 0000000000..472c5c0f56
--- /dev/null
+++ b/tools/jest-matchers/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@lg-tools/jest-matchers",
+ "version": "0.0.1",
+ "description": "LeafyGreen UI Kit Jest Matchers",
+ "main": "./dist/index.js",
+ "module": "./dist/esm/index.js",
+ "types": "./dist/index.d.ts",
+ "license": "Apache-2.0",
+ "scripts": {
+ "build": "lg-internal-build-package",
+ "tsc": "tsc --build tsconfig.json",
+ "create-matcher": "npx ts-node ./scripts/createNewMatcher.ts"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "chalk": "4.1.2",
+ "lodash": "^4.17.21"
+ },
+ "devDependencies": {
+ "@lg-tools/build": "0.3.0",
+ "commander": "^11.1.0",
+ "fs-extra": "^11.1.1",
+ "jsdom": "^22.1.0"
+ },
+ "peerDependencies": {
+ "@testing-library/react": "^14.0.0",
+ "@types/jest": "^29.5.0",
+ "jest": "^29.6.0"
+ },
+ "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/jest-matchers",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/mongodb/leafygreen-ui"
+ },
+ "bugs": {
+ "url": "https://jira.mongodb.org/projects/PD/summary"
+ }
+}
diff --git a/tools/jest-matchers/scripts/createNewMatcher.ts b/tools/jest-matchers/scripts/createNewMatcher.ts
new file mode 100644
index 0000000000..a3cf6e4f2a
--- /dev/null
+++ b/tools/jest-matchers/scripts/createNewMatcher.ts
@@ -0,0 +1,60 @@
+import chalk from 'chalk';
+import { Command } from 'commander';
+import fse from 'fs-extra';
+import { camelCase, lowerFirst } from 'lodash';
+import path from 'path';
+
+const cli = new Command();
+cli
+ .argument('', 'The name of the new matcher')
+ .action(createNewMatcher)
+ .parse();
+
+function createNewMatcher(matcherName: string) {
+ matcherName = lowerFirst(camelCase(matcherName));
+
+ if (!matcherName.startsWith('to')) {
+ console.warn(
+ chalk.yellow(
+ `Matcher names should start with the word "to". Received \`${matcherName}\``,
+ ),
+ );
+ }
+
+ const matchersDir = path.resolve(__dirname, '../src/matchers');
+
+ /** Create the matcher file */
+ const matchersFilePath = path.resolve(matchersDir, matcherName + '.ts');
+ const matcherFileTemplate = `
+import { createMatcher } from '../utils/createMatcher';
+
+export const ${matcherName} = createMatcher(function _${matcherName}() {
+ return {
+ pass: true,
+ message: () => '',
+ };
+});
+`;
+ fse.writeFileSync(matchersFilePath, matcherFileTemplate);
+
+ /** Create the test file */
+
+ const testFilePath = path.resolve(
+ __dirname,
+ '../src/tests',
+ matcherName + '.spec.ts',
+ );
+
+ const testFileTemplate = `
+describe('tools/jest-matchers/.${matcherName}', () => {
+ test.todo('')
+})
+`;
+ fse.writeFileSync(testFilePath, testFileTemplate);
+
+ /** Update the matchers index file */
+ const indexFilePath = path.resolve(matchersDir, 'index.ts');
+ let indexContents = fse.readFileSync(indexFilePath, 'utf-8');
+ indexContents += `export { ${matcherName} } from './${matcherName}';\n`;
+ fse.writeFileSync(indexFilePath, indexContents);
+}
diff --git a/tools/jest-matchers/scripts/tsconfig.json b/tools/jest-matchers/scripts/tsconfig.json
new file mode 100644
index 0000000000..d8ad23ab66
--- /dev/null
+++ b/tools/jest-matchers/scripts/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "module": "CommonJS",
+ "noEmit": true,
+ "tsBuildInfoFile": "./tsconfig.tsbuildinfo",
+ "incremental": true,
+ "target": "ES5",
+ "jsx": "react",
+ "allowJs": true,
+ "pretty": true,
+ "strictNullChecks": true,
+ "noUnusedLocals": false,
+ "esModuleInterop": true,
+ "strict": true,
+ "allowSyntheticDefaultImports": true,
+ "moduleResolution": "node",
+ "baseUrl": ".",
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ }
+}
\ No newline at end of file
diff --git a/tools/jest-matchers/src/index.ts b/tools/jest-matchers/src/index.ts
new file mode 100644
index 0000000000..67df2d899f
--- /dev/null
+++ b/tools/jest-matchers/src/index.ts
@@ -0,0 +1,3 @@
+import * as matchers from './matchers';
+
+expect.extend(matchers);
diff --git a/tools/jest-matchers/src/matchers/index.ts b/tools/jest-matchers/src/matchers/index.ts
new file mode 100644
index 0000000000..e01e47eac2
--- /dev/null
+++ b/tools/jest-matchers/src/matchers/index.ts
@@ -0,0 +1,2 @@
+export { toBeLabelled } from './toBeLabelled';
+export { toSpreadRest } from './toSpreadRest';
diff --git a/tools/jest-matchers/src/matchers/toBeLabelled.ts b/tools/jest-matchers/src/matchers/toBeLabelled.ts
new file mode 100644
index 0000000000..344bed5b3d
--- /dev/null
+++ b/tools/jest-matchers/src/matchers/toBeLabelled.ts
@@ -0,0 +1,57 @@
+import { isNull } from 'lodash';
+
+import { createMatcher } from '../utils/createMatcher';
+
+const isValidString = (str: any): str is string =>
+ str && typeof str === 'string' && str.length > 0;
+
+/**
+ * Returns whether the provided element has at least one of the following:
+ * - a `label` attribute
+ * - an `aria-label` attribute
+ * - an `aria-labelledby` attribute with a valid associated element
+ * - an associated element `