diff --git a/README.md b/README.md index 4dc7ecc..8a0b291 100644 --- a/README.md +++ b/README.md @@ -193,7 +193,7 @@ The optionSnapshot is an object that contains the object state: `{ selected: boo ## Get options -You can fetch options asynchronously with the `getOptions` property. You can either return options directly or through a `Promise`. +You can fetch options asynchronously with the `getOptions` property. You can either return options directly or through a `Promise`. If a falsy value is returned, options will still be controlled by the `options` prop. ```jsx function getOptions(query) { diff --git a/__tests__/__snapshots__/0-Single.stories.storyshot b/__tests__/__snapshots__/0-Single.stories.storyshot new file mode 100644 index 0000000..0586ac0 --- /dev/null +++ b/__tests__/__snapshots__/0-Single.stories.storyshot @@ -0,0 +1,427 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Single select Always Open 1`] = ` +
+
+ +
+
+ +
+
+`; + +exports[`Storyshots Single select Default 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots Single select Group 1`] = ` +
+
+ +
+
+ +
+
+`; + +exports[`Storyshots Single select Limited Options 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots Single select Search 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots Single select Search With Empty Message 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots Single select Search With Empty Message Renderer 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots Single select Stay On Select 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots Single select With Placeholder 1`] = ` +
+
+ +
+
+`; diff --git a/__tests__/__snapshots__/storybook.test.js.snap b/__tests__/__snapshots__/1-Multiple.stories.storyshot similarity index 77% rename from __tests__/__snapshots__/storybook.test.js.snap rename to __tests__/__snapshots__/1-Multiple.stories.storyshot index 19a3414..6a7926f 100644 --- a/__tests__/__snapshots__/storybook.test.js.snap +++ b/__tests__/__snapshots__/1-Multiple.stories.storyshot @@ -1,94 +1,10 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Storyshots Async Fetch 1`] = ` -
-
- -
-
-`; - -exports[`Storyshots Async Fetch Multiple 1`] = ` -
-
- -
-
-
-
-`; - -exports[`Storyshots Custom Avatar Example 1`] = ` +exports[`Storyshots Multiple select Default 1`] = `
-
- -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • - -
  • -
  • @@ -465,26 +65,26 @@ exports[`Storyshots Custom Avatar Example 1`] = `
    `; -exports[`Storyshots Custom CSS Modules 1`] = ` +exports[`Storyshots Multiple select Default Value 1`] = `
    • -
    -`; - -exports[`Storyshots Custom Font Example 1`] = ` +exports[`Storyshots Multiple select Disabled 1`] = `
    @@ -622,408 +158,14 @@ exports[`Storyshots Custom Font Example 1`] = `
    `; -exports[`Storyshots Events Controlled Value 1`] = ` -Array [ -
    -
    - -
    -
    , -

    - You have selected: -

    , - , - , -] -`; - -exports[`Storyshots Events On Change 1`] = ` -Array [ -
    -
    - -
    -
    , -

    - Aa -

    , -] -`; - -exports[`Storyshots Hooks Custom Components 1`] = ` -
    - Size: -
    -`; - -exports[`Storyshots Misc Form 1`] = ` -Array [ -
    -
    -
    - -
    -
    -
    , -
    -
    - -
    -
    , -] -`; - -exports[`Storyshots Misc Numeric Values 1`] = ` +exports[`Storyshots Multiple select Group 1`] = `
    - -
    -
    -`; - -exports[`Storyshots Multiple select Default 1`] = ` -
    -
    -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    -`; - -exports[`Storyshots Multiple select Default Value 1`] = ` -
    -
    -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -
    -
    -`; - -exports[`Storyshots Multiple select Disabled 1`] = ` -
    -
    - -
    -
    -`; - -exports[`Storyshots Multiple select Group 1`] = ` -
    -
    @@ -5172,7 +4314,7 @@ exports[`Storyshots Multiple select Stay On Select With Single Initial Value 1`] placeholder="Select your items" readOnly={true} tabIndex="0" - value="" + value="Fries" />
    @@ -5262,429 +4404,3 @@ exports[`Storyshots Multiple select With Placeholder 1`] = `
    `; - -exports[`Storyshots Single select Always Open 1`] = ` -
    -
    - -
    -
    - -
    -
    -`; - -exports[`Storyshots Single select Default 1`] = ` -
    -
    - -
    -
    -`; - -exports[`Storyshots Single select Group 1`] = ` -
    -
    - -
    -
    - -
    -
    -`; - -exports[`Storyshots Single select Limited Options 1`] = ` -
    -
    - -
    -
    -`; - -exports[`Storyshots Single select Search 1`] = ` -
    -
    - -
    -
    -`; - -exports[`Storyshots Single select Search With Empty Message 1`] = ` -
    -
    - -
    -
    -`; - -exports[`Storyshots Single select Search With Empty Message Renderer 1`] = ` -
    -
    - -
    -
    -`; - -exports[`Storyshots Single select Stay On Select 1`] = ` -
    -
    - -
    -
    -`; - -exports[`Storyshots Single select With Placeholder 1`] = ` -
    -
    - -
    -
    -`; diff --git a/__tests__/__snapshots__/2-Events.stories.storyshot b/__tests__/__snapshots__/2-Events.stories.storyshot new file mode 100644 index 0000000..bf70d6b --- /dev/null +++ b/__tests__/__snapshots__/2-Events.stories.storyshot @@ -0,0 +1,129 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Events Controlled Value 1`] = ` +Array [ +
    +
    + +
    +
    , +

    + You have selected: +

    , + , + , +] +`; + +exports[`Storyshots Events On Change 1`] = ` +Array [ +
    +
    + +
    +
    , +

    + Aa +

    , +] +`; diff --git a/__tests__/__snapshots__/3-Custom.stories.storyshot b/__tests__/__snapshots__/3-Custom.stories.storyshot new file mode 100644 index 0000000..525ec89 --- /dev/null +++ b/__tests__/__snapshots__/3-Custom.stories.storyshot @@ -0,0 +1,559 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Custom Avatar Example 1`] = ` +
    +
    + +
    +
    + +
    +
    +`; + +exports[`Storyshots Custom CSS Modules 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots Custom Controllable Display 1`] = ` +
    +
    +
    + +
    +
    + +
    +`; + +exports[`Storyshots Custom Font Example 1`] = ` +
    +
    + +
    +
    +`; diff --git a/__tests__/__snapshots__/4-Async.stories.storyshot b/__tests__/__snapshots__/4-Async.stories.storyshot new file mode 100644 index 0000000..3b5f7f8 --- /dev/null +++ b/__tests__/__snapshots__/4-Async.stories.storyshot @@ -0,0 +1,129 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Async Controlled Fetch 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots Async Controlled Fetch Multiple 1`] = ` +
    +
    + +
    +
    +
    +
    +`; + +exports[`Storyshots Async Fetch 1`] = ` +
    +
    + +
    +
    +`; + +exports[`Storyshots Async Fetch Multiple 1`] = ` +
    +
    + +
    +
    +
    +
    +`; diff --git a/__tests__/__snapshots__/5-Hooks.stories.storyshot b/__tests__/__snapshots__/5-Hooks.stories.storyshot new file mode 100644 index 0000000..6c29669 --- /dev/null +++ b/__tests__/__snapshots__/5-Hooks.stories.storyshot @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Hooks Custom Components 1`] = ` +
    + Size: Small +
    +`; diff --git a/__tests__/__snapshots__/6-Misc.stories.storyshot b/__tests__/__snapshots__/6-Misc.stories.storyshot new file mode 100644 index 0000000..bb6803b --- /dev/null +++ b/__tests__/__snapshots__/6-Misc.stories.storyshot @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Misc Form 1`] = ` +Array [ +
    +
    +
    + +
    +
    +
    , +
    +
    + +
    +
    , +] +`; + +exports[`Storyshots Misc Numeric Values 1`] = ` +
    +
    + +
    +
    +`; diff --git a/__tests__/equal.test.js b/__tests__/equal.test.js new file mode 100644 index 0000000..013da06 --- /dev/null +++ b/__tests__/equal.test.js @@ -0,0 +1,759 @@ +import equal from '../src/lib/equal'; +import assert from 'assert'; + + +function func1() {} +function func2() {} +class MyMap extends Map {} +class MySet extends Set {} +var emptyObj = {}; +function map(obj, Class) { + var a = new (Class || Map); + for (var key in obj) + a.set(key, obj[key]); + return a; +} + +function myMap(obj) { + return map(obj, MyMap); +} + +function set(arr, Class) { + var a = new (Class || Set); + for (var value of arr) + a.add(value); + return a; +} + +function mySet(arr) { + return set(arr, MySet); +} + +var skipBigInt = typeof BigInt == 'undefined'; +var skipBigIntArray = typeof BigUint64Array == 'undefined'; + + +function testCases(equalFunc, suiteName, suiteTests) { + describe(suiteName, function() { + suiteTests.forEach(function (suite) { + describe(suite.description, function() { + suite.tests.forEach(function (test) { + (test.skip ? it.skip : it)(test.description, function() { + assert.strictEqual(equalFunc(test.value1, test.value2), test.equal); + }); + (test.skip ? it.skip : it)(test.description + ' (reverse arguments)', function() { + assert.strictEqual(equalFunc(test.value2, test.value1), test.equal); + }); + }); + }); + }); + }); +} + +const standard_tests = [ + { + description: 'scalars', + tests: [ + { + description: 'equal numbers', + value1: 1, + value2: 1, + equal: true + }, + { + description: 'not equal numbers', + value1: 1, + value2: 2, + equal: false + }, + { + description: 'number and array are not equal', + value1: 1, + value2: [], + equal: false + }, + { + description: '0 and null are not equal', + value1: 0, + value2: null, + equal: false + }, + { + description: 'equal strings', + value1: 'a', + value2: 'a', + equal: true + }, + { + description: 'not equal strings', + value1: 'a', + value2: 'b', + equal: false + }, + { + description: 'empty string and null are not equal', + value1: '', + value2: null, + equal: false + }, + { + description: 'null is equal to null', + value1: null, + value2: null, + equal: true + }, + { + description: 'equal booleans (true)', + value1: true, + value2: true, + equal: true + }, + { + description: 'equal booleans (false)', + value1: false, + value2: false, + equal: true + }, + { + description: 'not equal booleans', + value1: true, + value2: false, + equal: false + }, + { + description: '1 and true are not equal', + value1: 1, + value2: true, + equal: false + }, + { + description: '0 and false are not equal', + value1: 0, + value2: false, + equal: false + }, + { + description: 'NaN and NaN are equal', + value1: NaN, + value2: NaN, + equal: true + }, + { + description: '0 and -0 are equal', + value1: 0, + value2: -0, + equal: true + }, + { + description: 'Infinity and Infinity are equal', + value1: Infinity, + value2: Infinity, + equal: true + }, + { + description: 'Infinity and -Infinity are not equal', + value1: Infinity, + value2: -Infinity, + equal: false + } + ] + }, + + { + description: 'objects', + tests: [ + { + description: 'empty objects are equal', + value1: {}, + value2: {}, + equal: true + }, + { + description: 'equal objects (same properties "order")', + value1: {a: 1, b: '2'}, + value2: {a: 1, b: '2'}, + equal: true + }, + { + description: 'equal objects (different properties "order")', + value1: {a: 1, b: '2'}, + value2: {b: '2', a: 1}, + equal: true + }, + { + description: 'not equal objects (extra property)', + value1: {a: 1, b: '2'}, + value2: {a: 1, b: '2', c: []}, + equal: false + }, + { + description: 'not equal objects (different property values)', + value1: {a: 1, b: '2', c: 3}, + value2: {a: 1, b: '2', c: 4}, + equal: false + }, + { + description: 'not equal objects (different properties)', + value1: {a: 1, b: '2', c: 3}, + value2: {a: 1, b: '2', d: 3}, + equal: false + }, + { + description: 'equal objects (same sub-properties)', + value1: { a: [ { b: 'c' } ] }, + value2: { a: [ { b: 'c' } ] }, + equal: true + }, + { + description: 'not equal objects (different sub-property value)', + value1: { a: [ { b: 'c' } ] }, + value2: { a: [ { b: 'd' } ] }, + equal: false + }, + { + description: 'not equal objects (different sub-property)', + value1: { a: [ { b: 'c' } ] }, + value2: { a: [ { c: 'c' } ] }, + equal: false + }, + { + description: 'empty array and empty object are not equal', + value1: {}, + value2: [], + equal: false + }, + { + description: 'object with extra undefined properties are not equal #1', + value1: {}, + value2: {foo: undefined}, + equal: false + }, + { + description: 'object with extra undefined properties are not equal #2', + value1: {foo: undefined}, + value2: {}, + equal: false + }, + { + description: 'object with extra undefined properties are not equal #3', + value1: {foo: undefined}, + value2: {bar: undefined}, + equal: false + }, + { + description: 'nulls are equal', + value1: null, + value2: null, + equal: true + }, + { + description: 'null and undefined are not equal', + value1: null, + value2: undefined, + equal: false + }, + { + description: 'null and empty object are not equal', + value1: null, + value2: {}, + equal: false + }, + { + description: 'undefined and empty object are not equal', + value1: undefined, + value2: {}, + equal: false + }, + { + description: 'objects with different `toString` functions returning same values are equal', + value1: {toString: ()=>'Hello world!'}, + value2: {toString: ()=>'Hello world!'}, + equal: true + }, + { + description: 'objects with `toString` functions returning different values are not equal', + value1: {toString: ()=>'Hello world!'}, + value2: {toString: ()=>'Hi!'}, + equal: false + } + ] + }, + + { + description: 'arrays', + tests: [ + { + description: 'two empty arrays are equal', + value1: [], + value2: [], + equal: true + }, + { + description: 'equal arrays', + value1: [1, 2, 3], + value2: [1, 2, 3], + equal: true + }, + { + description: 'not equal arrays (different item)', + value1: [1, 2, 3], + value2: [1, 2, 4], + equal: false + }, + { + description: 'not equal arrays (different length)', + value1: [1, 2, 3], + value2: [1, 2], + equal: false + }, + { + description: 'equal arrays of objects', + value1: [{a: 'a'}, {b: 'b'}], + value2: [{a: 'a'}, {b: 'b'}], + equal: true + }, + { + description: 'not equal arrays of objects', + value1: [{a: 'a'}, {b: 'b'}], + value2: [{a: 'a'}, {b: 'c'}], + equal: false + }, + { + description: 'pseudo array and equivalent array are not equal', + value1: {'0': 0, '1': 1, length: 2}, + value2: [0, 1], + equal: false + } + ] + }, + { + description: 'Date objects', + tests: [ + { + description: 'equal date objects', + value1: new Date('2017-06-16T21:36:48.362Z'), + value2: new Date('2017-06-16T21:36:48.362Z'), + equal: true + }, + { + description: 'not equal date objects', + value1: new Date('2017-06-16T21:36:48.362Z'), + value2: new Date('2017-01-01T00:00:00.000Z'), + equal: false + }, + { + description: 'date and string are not equal', + value1: new Date('2017-06-16T21:36:48.362Z'), + value2: '2017-06-16T21:36:48.362Z', + equal: false + }, + { + description: 'date and object are not equal', + value1: new Date('2017-06-16T21:36:48.362Z'), + value2: {}, + equal: false + } + ] + }, + { + description: 'RegExp objects', + tests: [ + { + description: 'equal RegExp objects', + value1: /foo/, + value2: /foo/, + equal: true + }, + { + description: 'not equal RegExp objects (different pattern)', + value1: /foo/, + value2: /bar/, + equal: false + }, + { + description: 'not equal RegExp objects (different flags)', + value1: /foo/, + value2: /foo/i, + equal: false + }, + { + description: 'RegExp and string are not equal', + value1: /foo/, + value2: 'foo', + equal: false + }, + { + description: 'RegExp and object are not equal', + value1: /foo/, + value2: {}, + equal: false + } + ] + }, + { + description: 'functions', + tests: [ + { + description: 'same function is equal', + value1: func1, + value2: func1, + equal: true + }, + { + description: 'different functions are not equal', + value1: func1, + value2: func2, + equal: false + } + ] + }, + { + description: 'sample objects', + tests: [ + { + description: 'big object', + value1: { + prop1: 'value1', + prop2: 'value2', + prop3: 'value3', + prop4: { + subProp1: 'sub value1', + subProp2: { + subSubProp1: 'sub sub value1', + subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] + } + }, + prop5: 1000, + prop6: new Date(2016, 2, 10) + }, + value2: { + prop5: 1000, + prop3: 'value3', + prop1: 'value1', + prop2: 'value2', + prop6: new Date('2016/03/10'), + prop4: { + subProp2: { + subSubProp1: 'sub sub value1', + subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] + }, + subProp1: 'sub value1' + } + }, + equal: true + } + ] + } +]; + +const es6_tests = [ + { + description: 'bigint', + tests: [ + { + description: 'equal bigints', + value1: skipBigInt || BigInt(1), + value2: skipBigInt || BigInt(1), + equal: true, + skip: skipBigInt + }, + { + description: 'not equal bigints', + value1: skipBigInt || BigInt(1), + value2: skipBigInt || BigInt(2), + equal: false, + skip: skipBigInt + } + ] + }, + + { + description: 'Maps', + tests: [ + { + description: 'empty maps are equal', + value1: new Map, + value2: new Map, + equal: true + }, + { + description: 'empty maps of different class are not equal', + value1: new Map, + value2: new MyMap, + equal: false + }, + { + description: 'equal maps (same key "order")', + value1: map({a: 1, b: '2'}), + value2: map({a: 1, b: '2'}), + equal: true + }, + { + description: 'not equal maps (same key "order" - instances of different classes)', + value1: map({a: 1, b: '2'}), + value2: myMap({a: 1, b: '2'}), + equal: false + }, + { + description: 'equal maps (different key "order")', + value1: map({a: 1, b: '2'}), + value2: map({b: '2', a: 1}), + equal: true + }, + { + description: 'equal maps (different key "order" - instances of the same subclass)', + value1: myMap({a: 1, b: '2'}), + value2: myMap({b: '2', a: 1}), + equal: true + }, + { + description: 'not equal maps (extra key)', + value1: map({a: 1, b: '2'}), + value2: map({a: 1, b: '2', c: []}), + equal: false + }, + { + description: 'not equal maps (different key value)', + value1: map({a: 1, b: '2', c: 3}), + value2: map({a: 1, b: '2', c: 4}), + equal: false + }, + { + description: 'not equal maps (different keys)', + value1: map({a: 1, b: '2', c: 3}), + value2: map({a: 1, b: '2', d: 3}), + equal: false + }, + { + description: 'equal maps (same sub-keys)', + value1: map({ a: [ map({ b: 'c' }) ] }), + value2: map({ a: [ map({ b: 'c' }) ] }), + equal: true + }, + { + description: 'not equal maps (different sub-key value)', + value1: map({ a: [ map({ b: 'c' }) ] }), + value2: map({ a: [ map({ b: 'd' }) ] }), + equal: false + }, + { + description: 'not equal maps (different sub-key)', + value1: map({ a: [ map({ b: 'c' }) ] }), + value2: map({ a: [ map({ c: 'c' }) ] }), + equal: false + }, + { + description: 'empty map and empty object are not equal', + value1: {}, + value2: new Map, + equal: false + }, + { + description: 'map with extra undefined key is not equal #1', + value1: map({}), + value2: map({foo: undefined}), + equal: false + }, + { + description: 'map with extra undefined key is not equal #2', + value1: map({foo: undefined}), + value2: map({}), + equal: false + }, + { + description: 'maps with extra undefined keys are not equal #3', + value1: map({foo: undefined}), + value2: map({bar: undefined}), + equal: false + }, + { + description: 'null and empty map are not equal', + value1: null, + value2: new Map, + equal: false + }, + { + description: 'undefined and empty map are not equal', + value1: undefined, + value2: new Map, + equal: false + }, + { + description: 'map and a pseudo map are not equal', + value1: map({}), + value2: { + constructor: Map, + size: 0, + has: () => true, + get: () => 1, + }, + equal: false + }, + ] + }, + + { + description: 'Sets', + tests: [ + { + description: 'empty sets are equal', + value1: new Set, + value2: new Set, + equal: true + }, + { + description: 'empty sets of different class are not equal', + value1: new Set, + value2: new MySet, + equal: false + }, + { + description: 'equal sets (same value "order")', + value1: set(['a', 'b']), + value2: set(['a', 'b']), + equal: true + }, + { + description: 'not equal sets (same value "order" - instances of different classes)', + value1: set(['a', 'b']), + value2: mySet(['a', 'b']), + equal: false + }, + { + description: 'equal sets (different value "order")', + value1: set(['a', 'b']), + value2: set(['b', 'a']), + equal: true + }, + { + description: 'equal sets (different value "order" - instances of the same subclass)', + value1: mySet(['a', 'b']), + value2: mySet(['b', 'a']), + equal: true + }, + { + description: 'not equal sets (extra value)', + value1: set(['a', 'b']), + value2: set(['a', 'b', 'c']), + equal: false + }, + { + description: 'not equal sets (different values)', + value1: set(['a', 'b', 'c']), + value2: set(['a', 'b', 'd']), + equal: false + }, + { + description: 'not equal sets (different instances of objects)', + value1: set([ 'a', {} ]), + value2: set([ 'a', {} ]), + equal: false + }, + { + description: 'equal sets (same instances of objects)', + value1: set([ 'a', emptyObj ]), + value2: set([ 'a', emptyObj ]), + equal: true + }, + { + description: 'empty set and empty object are not equal', + value1: {}, + value2: new Set, + equal: false + }, + { + description: 'empty set and empty array are not equal', + value1: [], + value2: new Set, + equal: false + }, + { + description: 'set with extra undefined value is not equal #1', + value1: set([]), + value2: set([undefined]), + equal: false + }, + { + description: 'set with extra undefined value is not equal #2', + value1: set([undefined]), + value2: set([]), + equal: false + }, + { + description: 'set and pseudo set are not equal', + value1: new Set, + value2: { + constructor: Set, + size: 0, + has: () => true, + }, + equal: false + }, + ] + }, + + { + description: 'Typed arrays', + tests: [ + { + description: 'two empty arrays of the same class are equal', + value1: new Int32Array([]), + value2: new Int32Array([]), + equal: true + }, + { + description: 'two empty arrays of the different class are not equal', + value1: new Int32Array([]), + value2: new Int16Array([]), + equal: false + }, + { + description: 'equal arrays', + value1: new Int32Array([1, 2, 3]), + value2: new Int32Array([1, 2, 3]), + equal: true + }, + { + description: 'equal BigUint64Array arrays', + value1: skipBigIntArray || new BigUint64Array(['1', '2', '3']), + value2: skipBigIntArray || new BigUint64Array(['1', '2', '3']), + equal: true, + skip: skipBigIntArray + }, + { + description: 'not equal BigUint64Array arrays', + value1: skipBigIntArray || new BigUint64Array(['1', '2', '3']), + value2: skipBigIntArray || new BigUint64Array(['1', '2', '4']), + equal: false, + skip: skipBigIntArray + }, + { + description: 'not equal arrays (same items, different class)', + value1: new Int32Array([1, 2, 3]), + value2: new Int16Array([1, 2, 3]), + equal: false + }, + { + description: 'not equal arrays (different item)', + value1: new Int32Array([1, 2, 3]), + value2: new Int32Array([1, 2, 4]), + equal: false + }, + { + description: 'not equal arrays (different length)', + value1: new Int32Array([1, 2, 3]), + value2: new Int32Array([1, 2]), + equal: false + }, + { + description: 'pseudo array and equivalent typed array are not equal', + value1: {'0': 1, '1': 2, length: 2, constructor: Int32Array}, + value2: new Int32Array([1, 2]), + equal: false + } + ] + } +]; + +testCases(equal, 'equal - standard tests', standard_tests); +testCases(equal, 'equal - es6 tests', es6_tests); diff --git a/__tests__/storybook.test.js b/__tests__/storybook.test.js index 80d295c..2450126 100644 --- a/__tests__/storybook.test.js +++ b/__tests__/storybook.test.js @@ -1,3 +1,35 @@ -import initStoryShots from '@storybook/addon-storyshots'; +import React from 'react' +import initStoryShots, { Stories2SnapsConverter } from '@storybook/addon-storyshots'; +import { create, act } from 'react-test-renderer'; +import "regenerator-runtime/runtime"; +import fetchMock from "jest-fetch-mock" -initStoryShots(); +fetchMock.enableMocks() +fetch.mockResponse(JSON.stringify({ drinks: [] })) + +const wait = (amount = 0) => new Promise(resolve => setTimeout(resolve, amount)); + +const converter = new Stories2SnapsConverter(); + +initStoryShots({ + asyncJest: true, + test: async ({ story, context, done }) => { + let renderer; + act(() => { + // React.createElement() is important because of hooks [shouldn't call story.render() directly] + renderer = create(React.createElement(story.render), { + // Fix Portal / MUI Dialog issues + createNodeMock: (node) => document.createElement(node.type), + }); + }); + + // Let one render cycle pass before rendering snapshot + await act(() => wait(0)); + + // save each snapshot to a different file (similar to "multiSnapshotWithOptions") + const snapshotFileName = converter.getSnapshotFileName(context); + expect(renderer).toMatchSpecificSnapshot(snapshotFileName); + + done(); + }, +}); diff --git a/dist/cjs/useFetch.js b/dist/cjs/useFetch.js index f8b9b09..42bf617 100644 --- a/dist/cjs/useFetch.js +++ b/dist/cjs/useFetch.js @@ -41,12 +41,15 @@ function useFetch(q, defaultOptions, _ref) { return (0, _debounce["default"])(function (s) { var optionsReq = getOptions(s, defaultOptions); - setFetching(true); - Promise.resolve(optionsReq).then(function (newOptions) { - setOptions((0, _flattenOptions["default"])(filter(newOptions)(s))); - })["finally"](function () { - return setFetching(false); - }); + + if (optionsReq) { + setFetching(true); + Promise.resolve(optionsReq).then(function (newOptions) { + setOptions((0, _flattenOptions["default"])(filter(newOptions)(s))); + })["finally"](function () { + return setFetching(false); + }); + } }, debounceTime); }, [filterOptions, defaultOptions, getOptions, debounceTime]); (0, _react.useEffect)(function () { diff --git a/dist/esm/useFetch.js b/dist/esm/useFetch.js index 98cd840..1ccc4a8 100644 --- a/dist/esm/useFetch.js +++ b/dist/esm/useFetch.js @@ -17,10 +17,13 @@ export default function useFetch(q, defaultOptions, { return debounce(s => { const optionsReq = getOptions(s, defaultOptions); - setFetching(true); - Promise.resolve(optionsReq).then(newOptions => { - setOptions(flattenOptions(filter(newOptions)(s))); - }).finally(() => setFetching(false)); + + if (optionsReq) { + setFetching(true); + Promise.resolve(optionsReq).then(newOptions => { + setOptions(flattenOptions(filter(newOptions)(s))); + }).finally(() => setFetching(false)); + } }, debounceTime); }, [filterOptions, defaultOptions, getOptions, debounceTime]); useEffect(() => setOptions(defaultOptions), [defaultOptions]); diff --git a/package-lock.json b/package-lock.json index 6975b5d..1f6b513 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@storybook/addon-storysource": "^6.2.9", "@storybook/addons": "^6.2.9", "@storybook/react": "^6.2.9", + "assert": "^1.5.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", @@ -38,6 +39,7 @@ "fuse.js": "^3.4.5", "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", + "jest-fetch-mock": "^3.0.3", "pretty": "^2.0.0", "prop-types": "^15.7.2", "react": "^17.0.1", @@ -9855,6 +9857,15 @@ "react": "^0.14.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/cross-fetch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", + "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -12413,6 +12424,7 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -12468,7 +12480,7 @@ "version": "3.6.1", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } @@ -15157,6 +15169,16 @@ "node": ">= 10.14.2" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "26.3.0", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", @@ -20423,6 +20445,12 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, + "node_modules/promise-polyfill": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz", + "integrity": "sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==", + "dev": true + }, "node_modules/promise.allsettled": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.4.tgz", @@ -28768,6 +28796,7 @@ "integrity": "sha512-qOtwgiqI3LMqT0eXYNV6ykp7qSu0LQGeXxy3wOBGuDDqAizfgnAjomYEWGFcyKp5ahV7HCRCjxbixAklFPUmyw==", "dev": true, "requires": { + "@babel/core": "^7.12.10", "@babel/generator": "^7.12.11", "@babel/parser": "^7.12.11", "@babel/plugin-transform-react-jsx": "^7.12.12", @@ -33351,6 +33380,15 @@ "warning": "^4.0.3" } }, + "cross-fetch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", + "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "dev": true, + "requires": { + "node-fetch": "2.6.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -35525,7 +35563,7 @@ "version": "3.6.1", "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-3.6.1.tgz", "integrity": "sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==", - "devOptional": true + "dev": true }, "gauge": { "version": "2.7.4", @@ -37720,6 +37758,16 @@ "jest-util": "^26.6.2" } }, + "jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "requires": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "jest-get-type": { "version": "26.3.0", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", @@ -41819,6 +41867,12 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, + "promise-polyfill": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz", + "integrity": "sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==", + "dev": true + }, "promise.allsettled": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.4.tgz", diff --git a/package.json b/package.json index a85db58..6fb0801 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@storybook/addon-storysource": "^6.2.9", "@storybook/addons": "^6.2.9", "@storybook/react": "^6.2.9", + "assert": "^1.5.0", "babel-eslint": "^10.1.0", "babel-loader": "^8.1.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", @@ -69,6 +70,7 @@ "fuse.js": "^3.4.5", "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", + "jest-fetch-mock": "^3.0.3", "pretty": "^2.0.0", "prop-types": "^15.7.2", "react": "^17.0.1", diff --git a/src/Components/OptionsList.jsx b/src/Components/OptionsList.jsx index 70c80ec..65de229 100644 --- a/src/Components/OptionsList.jsx +++ b/src/Components/OptionsList.jsx @@ -49,6 +49,11 @@ const OptionsList = ({ ); +OptionsList.defaultProps = { + renderOption: null, + renderGroupHeader: null, +}; + OptionsList.propTypes = { options: PropTypes.arrayOf(optionType).isRequired, optionProps: PropTypes.shape({}).isRequired, diff --git a/src/lib/equal.js b/src/lib/equal.js new file mode 100644 index 0000000..ad28f93 --- /dev/null +++ b/src/lib/equal.js @@ -0,0 +1,69 @@ +/* eslint-disable no-restricted-syntax */ +// eslint-disable-next-line no-unused-vars, no-var +var envHasBigInt64Array = typeof BigInt64Array !== 'undefined'; + +module.exports = function equal(a, b) { + if (a === b) return true; + + if (a && b && typeof a === 'object' && typeof b === 'object') { + if (a.constructor !== b.constructor) return false; + + let length; let i; + if (Array.isArray(a)) { + length = a.length; + // eslint-disable-next-line eqeqeq + if (length != b.length) return false; + // eslint-disable-next-line no-plusplus + for (i = length; i-- !== 0;) if (!equal(a[i], b[i])) return false; + return true; + } + + if ((a instanceof Map) && (b instanceof Map)) { + if (a.size !== b.size) return false; + for (i of a.entries()) if (!b.has(i[0])) return false; + for (i of a.entries()) if (!equal(i[1], b.get(i[0]))) return false; + return true; + } + + if ((a instanceof Set) && (b instanceof Set)) { + if (a.size !== b.size) return false; + for (i of a.entries()) if (!b.has(i[0])) return false; + return true; + } + + if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { + length = a.length; + // eslint-disable-next-line eqeqeq + if (length != b.length) return false; + // eslint-disable-next-line no-plusplus + for (i = length; i-- !== 0;) if (a[i] !== b[i]) return false; + return true; + } + + if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; + if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); + if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); + + const keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) return false; + + // eslint-disable-next-line no-plusplus + for (i = length; i-- !== 0;) { + if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; + } + + // eslint-disable-next-line no-plusplus + for (i = length; i-- !== 0;) { + const key = keys[i]; + + if (!equal(a[key], b[key])) return false; + } + + return true; + } + + // true if both NaN, false otherwise + // eslint-disable-next-line no-self-compare + return a !== a && b !== b; +}; diff --git a/src/lib/getOptions.js b/src/lib/getOptions.js index 97d5415..9ae1bb6 100644 --- a/src/lib/getOptions.js +++ b/src/lib/getOptions.js @@ -26,6 +26,7 @@ export default function getOptions(value, oldValue, options, multiple) { } newOptions.forEach((newOption) => { + // eslint-disable-next-line eqeqeq const optionIndex = oldOptions.findIndex((o) => o.value == newOption.value); if (optionIndex >= 0) { diff --git a/src/useFetch.js b/src/useFetch.js index 0cff3af..8fe0a87 100644 --- a/src/useFetch.js +++ b/src/useFetch.js @@ -18,14 +18,15 @@ export default function useFetch(q, defaultOptions, { return debounce((s) => { const optionsReq = getOptions(s, defaultOptions); + if (optionsReq) { + setFetching(true); - setFetching(true); - - Promise.resolve(optionsReq) - .then((newOptions) => { - setOptions(flattenOptions(filter(newOptions)(s))); - }) - .finally(() => setFetching(false)); + Promise.resolve(optionsReq) + .then((newOptions) => { + setOptions(flattenOptions(filter(newOptions)(s))); + }) + .finally(() => setFetching(false)); + } }, debounceTime); }, [filterOptions, defaultOptions, getOptions, debounceTime]); diff --git a/src/useSelect.js b/src/useSelect.js index 28bcf6f..42015a3 100644 --- a/src/useSelect.js +++ b/src/useSelect.js @@ -10,6 +10,7 @@ import getDisplayValue from './lib/getDisplayValue'; import useFetch from './useFetch'; import getValues from './lib/getValues'; import useHighlight from './useHighlight'; +import equal from './lib/equal'; export default function useSelect({ value: defaultValue = null, @@ -98,18 +99,19 @@ export default function useSelect({ }), [onMouseDown]); useEffect(() => { - if (valueRef.current === defaultValue) { - return; - } - - valueRef.current = defaultValue; - - setValue(getOptions( + const newValue = getOptions( defaultValue, null, options, multiple, - )); + ); + + if (equal(valueRef.current, newValue)) { + return; + } + + valueRef.current = newValue; + setValue(newValue); }, [defaultValue, multiple, options]); return [snapshot, valueProps, optionProps, setValue]; diff --git a/stories/4-Async.stories.js b/stories/4-Async.stories.js index 1898524..6367d24 100644 --- a/stories/4-Async.stories.js +++ b/stories/4-Async.stories.js @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import SelectSearch from '../src'; import '../style.css'; import { countries, fontStacks } from './data'; @@ -42,3 +43,70 @@ export const FetchMultiple = () => ( placeholder="Your favorite drink" /> ); + +export const ControlledFetch = () => { + const [options, setOptions] = useState([]) + const [value, setValue] = useState() + const [valueOption, setValueOption] = useState() + const [query, setQuery] = useState('') + + useEffect(() => { + fetch(`https://www.thecocktaildb.com/api/json/v1/1/search.php?s=${query}`) + .then(response => response.json()) + .then(({ drinks }) => { + const newOptions = drinks.map(({ idDrink, strDrink }) => ({ value: idDrink, name: strDrink })) + if (valueOption) newOptions.unshift(valueOption) + setOptions(newOptions) + }) + .catch((e) => console.error(e)); + }, [query]) + + return( + { + setValue(value) + setValueOption(options.find((o) => value === o.value)) + }} + getOptions={(query) => { + setQuery(query) + }} + search + placeholder="Your favorite drink" + /> + ) +}; + +export const ControlledFetchMultiple = () => { + const [options, setOptions] = useState([]) + const [value, setValue] = useState([]) + const [valueOptions, setValueOptions] = useState([]) + const [query, setQuery] = useState('') + + useEffect(() => { + fetch(`https://www.thecocktaildb.com/api/json/v1/1/search.php?s=${query}`) + .then(response => response.json()) + .then(({ drinks }) => { + setOptions([...valueOptions, ...drinks.map(({ idDrink, strDrink }) => ({ value: idDrink, name: strDrink }))]) + }) + .catch((e) => console.error(e)); + }, [query]) + + return( + { + setValue(value) + setValueOptions(options.filter((o) => value.includes(o.value))) + }} + multiple + getOptions={(query) => { + setQuery(query) + }} + search + placeholder="Your favorite drink" + /> + ) +}; \ No newline at end of file