Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
testEnvironment: 'jsdom',
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
"devDependencies": {
"@testing-library/jest-dom": "^4.2.3",
"@testing-library/react": "^9.3.2",
"@types/dot-object": "^1.7.0",
"@types/history": "^4.7.3",
"@types/jest": "^24.0.22",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.3",
"@types/react-redux": "^7.1.5",
"@typescript-eslint/eslint-plugin": "^2.6.0",
"@typescript-eslint/parser": "^2.6.0",
"dot-object": "^2.1.2",
"eslint": "^6.6.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-prettier": "^6.5.0",
Expand All @@ -57,6 +59,7 @@
"lint-staged": "^9.4.2",
"prettier": "^1.18.2",
"react": "^16.11.0",
"react-dom": "^16.12.0",
"react-redux": "^7.1.1",
"rollup": "^1.26.0",
"rollup-plugin-babel": "^4.3.3",
Expand Down
4 changes: 2 additions & 2 deletions src/components/Empty/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export interface Props {
className?: string;
}

function Empty({ className }: Props) {
return <div className={className}>There is no data</div>;
function Empty({ className, ...other }: Props) {
return <div className={className} { ...other}>There is no data</div>;
}

export default memo(Empty);
85 changes: 85 additions & 0 deletions src/components/List/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { ReactNode } from "react";
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import List, { Props as ListProps } from "./index";

interface TestRowProps {
id: number;
label: string;
}

function TestRow({ id, label }: TestRowProps) {
return (
<div>
{id} - {label}
</div>
);
}

interface TestEmptyProps {
className?: string;
message: string;
}

function TestEmpty({ message }: TestEmptyProps) {
return (
<>
<span>custom message:</span> {message}
</>
);
}

const sharedProps: ListProps<TestRowProps, TestRowProps> = {
RowComponent: TestRow,
rows: [
{ id: 10, label: "A" },
{ id: 11, label: "B" },
{ id: 12, label: "C" }
]
};

describe("The List component tests", () => {
test("It should render custom EmptyComponent and message", () => {
const { queryByText } = render(
<List<TestRowProps, TestRowProps, TestEmptyProps>
{...sharedProps}
rows={[]}
EmptyComponent={TestEmpty}
EmptyProps={{ message: "No Data" }}
/>
);
expect(queryByText("custom message:")).toBeInTheDocument();
});

test("It should not render EmptyComponent if hasEmpty is false", () => {
const { queryByTestId } = render(
<List {...sharedProps} rows={[]} hasEmpty={false} />
);
expect(queryByTestId("empty-component")).toBeNull();
});

test("It should render firstChild and lastChild at correct positions", () => {
const firstChild = <span data-testid="first-child" />;
const lastChild = <span data-testid="last-child" />;
const { getByTestId } = render(
<List {...sharedProps} firstChild={firstChild} lastChild={lastChild} />
);
expect(getByTestId("root").firstChild).toBe(getByTestId("first-child"));
expect(getByTestId("root").lastChild).toBe(getByTestId("last-child"));
});

test("It should render custom container", () => {
function CustomContainer({ children }: { children: ReactNode }) {
return (
<section data-testid="custom-root">
{children}
</section>
);
}
const { getByTestId } = render(
<List {...sharedProps} Container={CustomContainer} />
);

expect(getByTestId("custom-root")).toBeInTheDocument();
});
});
10 changes: 6 additions & 4 deletions src/components/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { ComponentType, memo, ReactNode } from "react";
import React, { ComponentType, memo, ReactNode, ElementType } from "react";
import ListRows, { Props as ListRowsProps } from "../ListRows";
import Empty, { Props as EmptyProps } from "../Empty";

export interface Props<T, U = T, V extends EmptyProps = EmptyProps>
extends ListRowsProps<T, U> {
className?: string;
Container?: ComponentType<{ className?: string; children: ReactNode }> | ElementType;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ElementType just cause type checking ignoring ComponentType<{ className?: string; children: ReactNode }>. can't we cast div to solve the problem?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right about type checking issue, I fixed this prop type. Now it's possible to set all HTML tags + valid react components that have required props.
Are you Ok with this idea?

EmptyComponent?: ComponentType<V>;
EmptyProps?: V;
hasEmpty?: boolean;
Expand All @@ -14,6 +15,7 @@ export interface Props<T, U = T, V extends EmptyProps = EmptyProps>

function BList<T, U = T, V extends EmptyProps = EmptyProps>({
className,
Container = 'div',
EmptyComponent = Empty,
EmptyProps,
hasEmpty = true,
Expand All @@ -23,14 +25,14 @@ function BList<T, U = T, V extends EmptyProps = EmptyProps>({
...other
}: Props<T, U, V>) {
return (
<div className={className}>
<Container data-testid="root" className={className}>
{firstChild}
<ListRows rows={rows} {...other} />
{hasEmpty && rows.length === 0 && (
<EmptyComponent {...(EmptyProps as V)} />
<EmptyComponent data-testid="empty-component" {...(EmptyProps as V)} />
)}
{lastChild}
</div>
</Container>
);
}

Expand Down
79 changes: 79 additions & 0 deletions src/components/ListRows/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import ListRows, { Props as ListRowsProps } from "./index";

interface TestRowProps {
person: { code: number };
name: string;
family: string;
id: unknown;
}

function TestRow({ person: { code }, name, family }: TestRowProps) {
return (
<div>
{code} - {name} {family}
</div>
);
}

const sharedProps: ListRowsProps<TestRowProps> = {
RowComponent: TestRow,
rows: [
{ person: { code: 11 }, name: "Sara", family: "Doe", id: 11 },
{ person: { code: 12 }, name: "John", family: "Doe", id: 12 }
],
dataKey: "person.code"
};

const originalError = console.error;
afterEach(() => (console.error = originalError));

describe("The ListRows component tests", () => {
test("It should throw an Error if the dataKey does not exist", () => {
console.error = jest.fn();
try {
render(<ListRows {...sharedProps} dataKey="invalid.key" />);
} catch (e) {
expect(e.message).toBe("The `invalid.key` property does not exist");
}
});

test("It should throw an Error if the type of dataKey property is not number or string", () => {
const keys = [
{ key: 11, error: false },
{ key: "11", error: false },
{ key: true, error: true },
{ key: () => {}, error: true },
{ key: new Date(), error: true },
{ key: { test: { a: "b" } }, error: true }
];

console.error = jest.fn();

keys.forEach(record => {
let hasError = false;
try {
render(
<ListRows
{...sharedProps}
RowProps={row => ({ ...row, id: record.key })}
dataKey="id"
/>
);
} catch (e) {
hasError =
e.message === "The type of `id` property must be string or number";
}

expect(hasError).toBe(record.error);
});
});

test("It should render rows correctly", () => {
const { queryByText } = render(<ListRows {...sharedProps} />);
expect(queryByText("11 - Sara Doe")).toBeInTheDocument();
expect(queryByText("12 - John Doe")).toBeInTheDocument();
});
});
16 changes: 9 additions & 7 deletions src/components/ListRows/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { memo, ComponentType, Key } from "react";
import dot from "dot-object";

export interface Props<T, U = T> {
rows: T[];
Expand All @@ -16,17 +17,18 @@ function BListRows<T, U = T>({
return (
<>
{rows.map(row => {
// const key = dot.pick(dataKey, row); // dot-object
const key = row[dataKey];
const rowProps = RowProps ? RowProps(row) : row;
const key = dot.pick(dataKey, rowProps);

if (typeof key !== "string" && typeof key !== "number") {
throw new Error("Invalid row key");
if(key===undefined) {
throw new Error("The `"+ dataKey +"` property does not exist");
} else if (typeof key !== "string" && typeof key !== "number") {
throw new Error("The type of `"+ dataKey +"` property must be string or number");
}

const rowProps = RowProps ? RowProps(row) : row;

return (
<RowComponent
key={(row[dataKey] as unknown) as Key}
key={(key as unknown) as Key}
{...(rowProps as U)}
/>
);
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./components/ListRows";
Loading