Skip to content

Commit

Permalink
options to tweak timeouts for "find" and "findAll", simpler error mes…
Browse files Browse the repository at this point in the history
…sage definition
  • Loading branch information
nknapp committed Apr 21, 2024
1 parent 485c13f commit 508d0e6
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 55 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

# Upcoming

- Prepare changelog and publish automation
## Features

- options to tweak timeouts for "find" and "findAll"
- simpler error message definition

# v0.0.3

Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"name": "query-bin",
"version": "0.0.3",
"description": "A list with queries like in the 'testing-library'",
"main": "index.js",
"types": "index.d.s",
"type": "module",
"module": "index.js",
Expand Down
12 changes: 9 additions & 3 deletions src/QueryBin.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class QueryBin<T, Q extends QueryDefinitions<T> = QueryDefinitions<T>> {
constructor(queries: Q);
constructor(queries: Q, options?: Options);

add(value: T): void;

Expand All @@ -15,14 +15,20 @@ export class QueryBin<T, Q extends QueryDefinitions<T> = QueryDefinitions<T>> {
find: Find<T, Q>;
}

interface Options {
retryDelayMillis?: number;
timeoutMillis?: number;
}

export type Queries<T> = {
[name: string]: (...args: any[]) => QueryDefinition<T>;
};

export type QueryDefinition<T> = {
queryAll: (all: T[]) => T[];
onNoneFound(all: T[]): Error;
onMultipleFound(all: T[], found: T[]): Error;
serializeForErrorMessage: (item: T) => string;
noneFoundMessage: string;
multipleFoundMessage: string;
};

export type QueryAll<T, Q extends QueryDefinitions<T>> = {
Expand Down
51 changes: 35 additions & 16 deletions src/QueryBin.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
export class QueryBin {
constructor(queries = {}) {
constructor(
queries = {},
{ retryDelayMillis = 50, timeoutMillis = 1000 } = {},
) {
this.values = [];
this.retryDelayMillis = retryDelayMillis;
this.timeoutMillis = timeoutMillis;
this.queryAll = mapValues(queries, (factory) => {
return (...args) => {
return factory(...args).queryAll(this.values);
const { queryAll } = factory(...args);
return queryAll(this.values);
};
});
this.query = mapValues(queries, (factory) => {
Expand All @@ -12,27 +18,25 @@ export class QueryBin {
const results = queryDefinition.queryAll(this.values);
if (results.length === 0) return null;
if (results.length > 1)
throw queryDefinition.onMultipleFound(this.values, results);
throw this.multipleResultsError(queryDefinition, results);
return results[0];
};
});
this.get = mapValues(queries, (factory) => {
return (...args) => {
const queryDefinition = factory(...args);
const results = queryDefinition.queryAll(this.values);
if (results.length === 0)
throw queryDefinition.onNoneFound(this.values);
if (results.length === 0) throw this.noResultsError(queryDefinition);
if (results.length > 1)
throw queryDefinition.onMultipleFound(this.values, results);
throw this.multipleResultsError(queryDefinition, results);
return results[0];
};
});
this.getAll = mapValues(queries, (factory) => {
return (...args) => {
const queryDefinition = factory(...args);
const results = queryDefinition.queryAll(this.values);
if (results.length === 0)
throw queryDefinition.onNoneFound(this.values);
if (results.length === 0) throw this.noResultsError(queryDefinition);
return results;
};
});
Expand All @@ -41,8 +45,7 @@ export class QueryBin {
const queryDefinition = factory(...args);
return this.waitFor(() => {
const results = queryDefinition.queryAll(this.values);
if (results.length === 0)
throw queryDefinition.onNoneFound(this.values);
if (results.length === 0) throw this.noResultsError(queryDefinition);
return results;
});
};
Expand All @@ -52,10 +55,9 @@ export class QueryBin {
const queryDefinition = factory(...args);
return this.waitFor(() => {
const results = queryDefinition.queryAll(this.values);
if (results.length === 0)
throw queryDefinition.onNoneFound(this.values);
if (results.length === 0) throw this.noResultsError(queryDefinition);
if (results.length > 1)
throw queryDefinition.onMultipleFound(this.values, results);
throw this.multipleResultsError(queryDefinition, results);
return results[0];
});
};
Expand All @@ -74,16 +76,33 @@ export class QueryBin {
return this.values;
}

multipleResultsError(queryDefinition, results) {
return new Error(
queryDefinition.multipleFoundMessage +
"\nFound: \n" +
results.map(queryDefinition.serializeForErrorMessage).join("\n"),
);
}

noResultsError(queryDefinition) {
const values =
this.values.length === 0
? "List is completely empty!"
: "All values: \n" +
this.values.map(queryDefinition.serializeForErrorMessage).join("\n");
return new Error(`${queryDefinition.noneFoundMessage}\n${values}`);
}

async waitFor(fn) {
const started = Date.now();
while (Date.now() - started < 1000) {
while (Date.now() - started <= this.timeoutMillis) {
try {
return await fn();
} catch (e) {
await delay(100);
await delay(this.retryDelayMillis);
}
}
return fn();
await fn();
}
}

Expand Down
117 changes: 91 additions & 26 deletions src/QueryBin.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { describe, expect, it } from "vitest";
import { QueryDefinition, QueryBin } from "./QueryBin";
import { QueryBin, QueryDefinition } from "./QueryBin";

function dividableBy(modulo: number): QueryDefinition<number> {
return {
queryAll: (all: number[]): number[] => all.filter((n) => n % modulo === 0),
onMultipleFound: (all, found) =>
new Error(
`Multiple numbers of ${all} are dividable by ${modulo}: ${found}`,
),
onNoneFound: (all) =>
new Error(
`Could not find any numbers in ${all} that are dividable by ${modulo}`,
),
serializeForErrorMessage: (item) => String(item),
noneFoundMessage: `No number is dividable by ${modulo}.`,
multipleFoundMessage: `Multiple numbers are dividable by ${modulo}.`,
};
}

Expand Down Expand Up @@ -52,7 +47,11 @@ describe("QueryBin", () => {
const bin = new QueryBin({ dividableBy });
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(() => bin.query.dividableBy(3)).toThrow(
"Multiple numbers of 1,2,3,4,5,6,7,8,9,10 are dividable by 3: 3,6,9",
"Multiple numbers are dividable by 3.\n" +
"Found: \n" +
"3\n" +
"6\n" +
"9",
);
});

Expand All @@ -72,15 +71,37 @@ describe("QueryBin", () => {
const bin = new QueryBin({ dividableBy });
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(() => bin.get.dividableBy(3)).toThrow(
"Multiple numbers of 1,2,3,4,5,6,7,8,9,10 are dividable by 3: 3,6,9",
"Multiple numbers are dividable by 3.\n" +
"Found: \n" +
"3\n" +
"6\n" +
"9",
);
});

it("'get' throws an error if no results match", () => {
const bin = new QueryBin({ dividableBy });
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(() => bin.get.dividableBy(25)).toThrow(
"Could not find any numbers in 1,2,3,4,5,6,7,8,9,10 that are dividable by 25",
"No number is dividable by 25.\n" +
"All values: \n" +
"1\n" +
"2\n" +
"3\n" +
"4\n" +
"5\n" +
"6\n" +
"7\n" +
"8\n" +
"9\n" +
"10",
);
});

it("'get' shows special message if there are no values at all", () => {
const bin = new QueryBin({ dividableBy });
expect(() => bin.get.dividableBy(25)).toThrow(
"No number is dividable by 25.\nList is completely empty!",
);
});

Expand All @@ -100,7 +121,18 @@ describe("QueryBin", () => {
const bin = new QueryBin({ dividableBy });
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(() => bin.getAll.dividableBy(25)).toThrow(
"Could not find any numbers in 1,2,3,4,5,6,7,8,9,10 that are dividable by 25",
"No number is dividable by 25.\n" +
"All values: \n" +
"1\n" +
"2\n" +
"3\n" +
"4\n" +
"5\n" +
"6\n" +
"7\n" +
"8\n" +
"9\n" +
"10",
);
});

Expand All @@ -117,10 +149,10 @@ describe("QueryBin", () => {
});

it("'findAll' throws an error if no results match", async () => {
const bin = new QueryBin({ dividableBy });
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
await expect(bin.findAll.dividableBy(25)).rejects.toThrow(
"Could not find any numbers in 1,2,3,4,5,6,7,8,9,10 that are dividable by 25",
const bin = new QueryBin({ dividableBy }, { timeoutMillis: 100 });
bin.addAll([1, 2]);
await expect(bin.findAll.dividableBy(3)).rejects.toThrow(
"No number is dividable by 3.\nAll values: \n1\n2",
);
});

Expand All @@ -138,18 +170,22 @@ describe("QueryBin", () => {
});

it("'find' throws an error on multiple results", async () => {
const bin = new QueryBin({ dividableBy });
const bin = new QueryBin({ dividableBy }, { timeoutMillis: 100 });
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
await expect(bin.find.dividableBy(3)).rejects.toThrow(
"Multiple numbers of 1,2,3,4,5,6,7,8,9,10 are dividable by 3: 3,6,9",
"Multiple numbers are dividable by 3.\n" +
"Found: \n" +
"3\n" +
"6\n" +
"9",
);
});

it("'find' throws an error if no results match", async () => {
const bin = new QueryBin({ dividableBy });
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
await expect(bin.findAll.dividableBy(25)).rejects.toThrow(
"Could not find any numbers in 1,2,3,4,5,6,7,8,9,10 that are dividable by 25",
const bin = new QueryBin({ dividableBy }, { timeoutMillis: 100 });
bin.addAll([1, 2]);
await expect(bin.findAll.dividableBy(3)).rejects.toThrow(
"No number is dividable by 3.\nAll values: \n1\n2",
);
});

Expand All @@ -161,8 +197,37 @@ describe("QueryBin", () => {

it("'find' return multiple results if they come in later", async () => {
const bin = new QueryBin({ dividableBy });
const result = bin.find.dividableBy(7);
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(await result).toEqual(7);
setTimeout(() => {
bin.addAll([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
}, 100);
expect(await bin.find.dividableBy(7)).toEqual(7);
});

it("'find' only waits 'timeoutMillis' milliseconds for items to appear", async () => {
const bin = new QueryBin({ dividableBy }, { timeoutMillis: 200 });
setTimeout(() => {
bin.addAll([1, 2, 3]);
}, 1000);
await expect(bin.find.dividableBy(3)).rejects.toThrow(
"No number is dividable by 3.\nList is completely empty!",
);
});

it("'find' waits about 1000 milliseconds by default", async () => {
const bin = new QueryBin({ dividableBy });
setTimeout(() => {
bin.addAll([1, 2, 3]);
}, 900);
expect(await bin.find.dividableBy(3)).toEqual(3);
});

it("'find' does not wait much longer than about 1000 milliseconds by default", async () => {
const bin = new QueryBin({ dividableBy });
setTimeout(() => {
bin.addAll([1, 2, 3]);
}, 1100);
await expect(bin.find.dividableBy(3)).rejects.toThrow(
"No number is dividable by 3.\nList is completely empty!",
);
});
});
11 changes: 3 additions & 8 deletions src/example.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,9 @@ function byMethodAndUrl(method: Method, url: string): QueryDefinition<Request> {
return {
queryAll: (items) =>
items.filter((item) => item.method === method && item.url.includes(url)),
onNoneFound: (all) =>
new Error(
`Could not find item with method ${method} and URL containing ${url}. All requests:\n\n${JSON.stringify(all, null, 2)}`,
),
onMultipleFound: (all, found) =>
new Error(
`Multiple items found method ${method} and URL containing ${url}. Found:\n\n${JSON.stringify(found, null, 2)}`,
),
serializeForErrorMessage: (item) => JSON.stringify(item, null, 2),
noneFoundMessage: `Could not find requests with method ${method} and URL containing ${url}.`,
multipleFoundMessage: `Multiple requests found method ${method} and URL containing ${url}.`,
};
}

Expand Down

0 comments on commit 508d0e6

Please sign in to comment.