Repository: dubzzz/fast-check
Pull request #2056 (2021-06-30 21:26:31)

✨ Add built-in unmapper support onto `webUrl`
<!-- Context of the PR: short description and potentially linked issues -->

<!-- ...a few words to describe the content of this PR...               -->
<!-- ... -->

<!-- Type of PR: [ ] unchecked / [ ] checked -->
**_Category:_**

- [x] ✨ Introduce new features
- [ ] 📝 Add or update documentation
- [ ] ✅ Add or update tests
- [ ] 🐛 Fix a bug
- [ ] 🏷️ Add or update types
- [ ] ⚡️ Improve performance
- [ ] _Other(s):_ ...
<!-- Don't forget to add the gitmoji icon in the name of the PR -->
<!-- See: https://gitmoji.dev/                                  -->

<!-- Fixing bugs, adding features... may impact existing ones           -->
<!-- in order to track potential issues that could be related to your PR -->
<!-- please check the impacts and describe more precisely what to expect -->

**_Potential impacts:_**

<!-- Generated values: Can your change impact any of the existing generators in terms of generated values, if so which ones? when? -->
<!-- Shrink values:    Can your change impact any of the existing generators in terms of shrink values, if so which ones? when? -->
<!-- Performance:      Can it require some typings changes on user side? Please give more details -->
<!-- Typings:          Is there a potential performance impact? In which cases? -->
- [ ] Generated values
- [ ] Shrink values
- [ ] Performance
- [ ] Typings
- [ ] _Other(s):_ ...


codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/2056/builds/145689) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit da19639456e759ebed6012a41405ee80d75c0234:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-fyhc7)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-r6qgm)| Configuration |


Patch:
diff --git a/.github/workflows/build-status.yml b/.github/workflows/build-status.yml
index c30cce56..9c7af7ab 100644
--- a/.github/workflows/build-status.yml
+++ b/.github/workflows/build-status.yml
@@ -25,7 +25,7 @@ jobs:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
       - name: Install dependencies
@@ -47,7 +47,7 @@ jobs:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}-${{matrix.node-version}}
       - name: Using Node v${{matrix.node-version}}
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: ${{matrix.node-version}}
       - name: Install dependencies
@@ -84,7 +84,7 @@ jobs:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}-${{matrix.node-version}}
       - name: Using Node v${{matrix.node-version}}
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: ${{matrix.node-version}}
       - name: Install dependencies
@@ -106,7 +106,7 @@ jobs:
     steps:
       - uses: actions/checkout@v2.3.4
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
       - name: Check package score using skypack
@@ -121,7 +121,7 @@ jobs:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}-v16.x
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
       - name: Install dependencies
@@ -149,7 +149,7 @@ jobs:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}-v16.x
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
       - name: Install dependencies
@@ -174,7 +174,7 @@ jobs:
     steps:
       - uses: actions/checkout@v2.3.4
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
       - name: Download production package
@@ -206,7 +206,7 @@ jobs:
     steps:
       - uses: actions/checkout@v2.3.4
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
       - name: Download production package
@@ -238,7 +238,7 @@ jobs:
     steps:
       - uses: actions/checkout@v2.3.4
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
       - name: Download production package
@@ -250,7 +250,7 @@ jobs:
         with:
           path: 'test/legacy/node-8'
       - name: Using Node v8.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '8.x'
       - name: Test test/legacy/node-8/
@@ -372,7 +372,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
           registry-url: 'https://registry.npmjs.org'
diff --git a/.github/workflows/request-deploy-netlify.yml b/.github/workflows/request-deploy-netlify.yml
index c387b527..9929fccc 100644
--- a/.github/workflows/request-deploy-netlify.yml
+++ b/.github/workflows/request-deploy-netlify.yml
@@ -27,7 +27,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
       - name: Using Node v16.x
-        uses: actions/setup-node@v2.1.5
+        uses: actions/setup-node@v2.2.0
         with:
           node-version: '16.x'
       - name: Install fast-check dependencies
diff --git a/src/arbitrary/_internals/mappers/PartsToUrl.ts b/src/arbitrary/_internals/mappers/PartsToUrl.ts
new file mode 100644
index 00000000..0b1b0291
--- /dev/null
+++ b/src/arbitrary/_internals/mappers/PartsToUrl.ts
@@ -0,0 +1,34 @@
+/** @internal */
+export function partsToUrlMapper(data: [string, string, string, string | null, string | null]): string {
+  const [scheme, authority, path] = data;
+  const query = data[3] === null ? '' : `?${data[3]}`;
+  const fragments = data[4] === null ? '' : `#${data[4]}`;
+  return `${scheme}://${authority}${path}${query}${fragments}`;
+}
+
+/** @internal More details on RFC 3986, https://www.ietf.org/rfc/rfc3986.txt */
+const UrlSplitRegex =
+  /^([[A-Za-z][A-Za-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)(\?[A-Za-z0-9\-._~!$&'()*+,;=:@/?%]*)?(#[A-Za-z0-9\-._~!$&'()*+,;=:@/?%]*)?$/;
+
+/** @internal */
+export function partsToUrlUnmapper(value: unknown): [string, string, string, string | null, string | null] {
+  if (typeof value !== 'string') {
+    throw new Error('Incompatible value received: type');
+  }
+  const m = UrlSplitRegex.exec(value);
+  if (m === null) {
+    throw new Error('Incompatible value received');
+  }
+  const scheme = m[1];
+  const authority = m[2];
+  const path = m[3];
+  const query: string | undefined = m[4];
+  const fragments: string | undefined = m[5];
+  return [
+    scheme,
+    authority,
+    path,
+    query !== undefined ? query.substring(1) : null,
+    fragments !== undefined ? fragments.substring(1) : null,
+  ];
+}
diff --git a/src/arbitrary/_internals/mappers/SegmentsToPath.ts b/src/arbitrary/_internals/mappers/SegmentsToPath.ts
new file mode 100644
index 00000000..1d374103
--- /dev/null
+++ b/src/arbitrary/_internals/mappers/SegmentsToPath.ts
@@ -0,0 +1,15 @@
+/** @internal */
+export function segmentsToPathMapper(segments: string[]): string {
+  return segments.map((v) => `/${v}`).join('');
+}
+
+/** @internal */
+export function segmentsToPathUnmapper(value: unknown): string[] {
+  if (typeof value !== 'string') {
+    throw new Error('Incompatible value received: type');
+  }
+  if (value.length !== 0 && value[0] !== '/') {
+    throw new Error('Incompatible value received: start');
+  }
+  return value.split('/').splice(1);
+}
diff --git a/src/arbitrary/webUrl.ts b/src/arbitrary/webUrl.ts
index b5bfc421..9be3e9f1 100644
--- a/src/arbitrary/webUrl.ts
+++ b/src/arbitrary/webUrl.ts
@@ -8,6 +8,9 @@ import { webQueryParameters } from './webQueryParameters';
 import { webFragments } from './webFragments';
 import { webAuthority, WebAuthorityConstraints } from './webAuthority';
 import { webSegment } from './webSegment';
+import { convertFromNext, convertToNext } from '../check/arbitrary/definition/Converters';
+import { partsToUrlMapper, partsToUrlUnmapper } from './_internals/mappers/PartsToUrl';
+import { segmentsToPathMapper, segmentsToPathUnmapper } from './_internals/mappers/SegmentsToPath';
 
 /**
  * Constraints to be applied on {@link webUrl}
@@ -53,12 +56,16 @@ export function webUrl(constraints?: WebUrlConstraints): Arbitrary<string> {
   const validSchemes = c.validSchemes || ['http', 'https'];
   const schemeArb = constantFrom(...validSchemes);
   const authorityArb = webAuthority(c.authoritySettings);
-  const pathArb = array(webSegment()).map((p) => p.map((v) => `/${v}`).join(''));
-  return tuple(
-    schemeArb,
-    authorityArb,
-    pathArb,
-    c.withQueryParameters === true ? option(webQueryParameters()) : constant(null),
-    c.withFragments === true ? option(webFragments()) : constant(null)
-  ).map(([s, a, p, q, f]) => `${s}://${a}${p}${q === null ? '' : `?${q}`}${f === null ? '' : `#${f}`}`);
+  const pathArb = convertFromNext(convertToNext(array(webSegment())).map(segmentsToPathMapper, segmentsToPathUnmapper));
+  return convertFromNext(
+    convertToNext(
+      tuple(
+        schemeArb,
+        authorityArb,
+        pathArb,
+        c.withQueryParameters === true ? option(webQueryParameters()) : constant(null),
+        c.withFragments === true ? option(webFragments()) : constant(null)
+      )
+    ).map(partsToUrlMapper, partsToUrlUnmapper)
+  );
 }


Test patch:
diff --git a/test/unit/arbitrary/WebArbitrary.spec.ts b/test/unit/arbitrary/WebArbitrary.spec.ts
deleted file mode 100644
index 0ffbd22a..00000000
--- a/test/unit/arbitrary/WebArbitrary.spec.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { webUrl } from '../../../src/arbitrary/webUrl';
-
-import { URL } from 'url';
-import * as genericHelper from '../check/arbitrary/generic/GenericArbitraryHelper';
-import fc from '../../../lib/fast-check';
-
-const isValidUrl = (t: string) => {
-  // Valid url given the specs defined by WHATWG URL Standard
-  // https://url.spec.whatwg.org/
-  try {
-    // A TypeError will be thrown if the input is not a valid URL.
-    // https://nodejs.org/api/url.html#url_constructor_new_url_input_base
-    new URL(t);
-    return true;
-  } catch (err) {
-    return false;
-  }
-};
-
-describe('WebArbitrary', () => {
-  describe('webUrl', () => {
-    genericHelper.isValidArbitrary(webUrl, {
-      isValidValue: (g: string) => isValidUrl(g),
-      seedGenerator: fc.record(
-        {
-          validSchemes: fc.constant(['ftp']),
-          authoritySettings: fc.record(
-            {
-              withIPv4: fc.boolean(),
-              withIPv6: fc.boolean(),
-              withIPv4Extended: fc.boolean(),
-              withUserInfo: fc.boolean(),
-              withPort: fc.boolean(),
-            },
-            { withDeletedKeys: true }
-          ),
-          withQueryParameters: fc.boolean(),
-          withFragments: fc.boolean(),
-        },
-        { withDeletedKeys: true }
-      ),
-    });
-  });
-});
diff --git a/test/unit/arbitrary/__snapshots__/webUrl.spec.ts.snap b/test/unit/arbitrary/__snapshots__/webUrl.spec.ts.snap
new file mode 100644
index 00000000..b524042b
--- /dev/null
+++ b/test/unit/arbitrary/__snapshots__/webUrl.spec.ts.snap
@@ -0,0 +1,343 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`webUrl (integration) should be able to shrink http://my.domain.org/a/z 1`] = `
+"\\"http://my.domain.org/a/z\\"
+├> \\"http://domain.org/a/z\\"
+|  ├> \\"http://aomain.org/a/z\\"
+|  |  ├> \\"http://a.org/a/z\\"
+|  |  |  ├> \\"http://a.rg/a/z\\"
+|  |  |  |  ├> \\"http://a.ag/a/z\\"
+|  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  ├> \\"http://a.ag/z\\"
+|  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  ├> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |     └> \\"http://a.ag\\"
+|  |  |  |  |  |  ├> \\"http://a.ag/n\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag/h\\"
+|  |  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  ├> \\"http://a.ag/e\\"
+|  |  |  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  ├> \\"http://a.ag/c\\"
+|  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag/b\\"
+|  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |     |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |     └> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |        └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |           └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  └> \\"http://a.ag/d\\"
+|  |  |  |  |  |  |  |  |     ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |     ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |     |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |     └> \\"http://a.ag/c\\"
+|  |  |  |  |  |  |  |  |        ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |        ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |        ├> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |        |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |        |     └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |        └> \\"http://a.ag/b\\"
+|  |  |  |  |  |  |  |  |           ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |           |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           └> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |              ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |              └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |                 └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  └> \\"http://a.ag/g\\"
+|  |  |  |  |  |  |  |     ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |     ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |     |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |     └> \\"http://a.ag/f\\"
+|  |  |  |  |  |  |  |        ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |        ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |        |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |        └> \\"http://a.ag/e\\"
+|  |  |  |  |  |  |  |           ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |           |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           ├> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |           |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           |  └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |           |     └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           ├> \\"http://a.ag/c\\"
+|  |  |  |  |  |  |  |           |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |           |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           |  └> \\"http://a.ag/b\\"
+|  |  |  |  |  |  |  |           |     ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           |     ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |           |     |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           |     └> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |           |        ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           |        └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |           |           └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |           └> \\"http://a.ag/d\\"
+|  |  |  |  |  |  |  |              ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |              ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |              |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |              └> \\"http://a.ag/c\\"
+|  |  |  |  |  |  |  |                 ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |                 ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |                 |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |                 ├> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |                 |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |                 |  └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |                 |     └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |                 └> \\"http://a.ag/b\\"
+|  |  |  |  |  |  |  |                    └> …
+|  |  |  |  |  |  |  └> …
+|  |  |  |  |  |  └> …
+|  |  |  |  |  └> …
+|  |  |  |  └> …
+|  |  |  └> …
+|  |  └> …
+|  └> …
+└> …"
+`;
+
+exports[`webUrl (integration) should be able to shrink http://my.domain.org/a/z?query#fragments 1`] = `
+"\\"http://my.domain.org/a/z?query#fragments\\"
+├> \\"http://domain.org/a/z?query#fragments\\"
+|  ├> \\"http://aomain.org/a/z?query#fragments\\"
+|  |  ├> \\"http://a.org/a/z?query#fragments\\"
+|  |  |  ├> \\"http://a.rg/a/z?query#fragments\\"
+|  |  |  |  ├> \\"http://a.ag/a/z?query#fragments\\"
+|  |  |  |  |  ├> \\"http://a.ag?query#fragments\\"
+|  |  |  |  |  |  ├> \\"http://a.ag#fragments\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag#ments\\"
+|  |  |  |  |  |  |  |  ├> \\"http://a.ag#nts\\"
+|  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#ts\\"
+|  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#s\\"
+|  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#j\\"
+|  |  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#f\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#d\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag#c\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  |     └> \\"http://a.ag#b\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  |        └> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  |           └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag#e\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |     └> \\"http://a.ag#d\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        └> \\"http://a.ag#c\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |           ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |           └> \\"http://a.ag#b\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |              ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |              └> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |                 └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#h\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag#g\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |     └> \\"http://a.ag#f\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#d\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag#c\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |     ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |     └> \\"http://a.ag#b\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |        ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |        └> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        |           └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |        └> \\"http://a.ag#e\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |           ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |           └> \\"http://a.ag#d\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |              ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |              ├> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |              |  └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |              └> \\"http://a.ag#c\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |                 ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |                 └> \\"http://a.ag#b\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |                    ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |                    └> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |  |                       └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag#i\\"
+|  |  |  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |     └> \\"http://a.ag#h\\"
+|  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag#e\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  ├> \\"http://a.ag#c\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  |  ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  |  └> \\"http://a.ag#b\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  |     ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  |     └> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  |        └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag#d\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |     ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |     └> \\"http://a.ag#c\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |        ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |        ├> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |        |  └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |        └> \\"http://a.ag#b\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |           ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |           └> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |        |              └> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |        └> \\"http://a.ag#g\\"
+|  |  |  |  |  |  |  |  |  |  |  |           ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |           └> \\"http://a.ag#f\\"
+|  |  |  |  |  |  |  |  |  |  |  |              ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |              └> \\"http://a.ag#e\\"
+|  |  |  |  |  |  |  |  |  |  |  |                 ├> \\"http://a.ag#\\"
+|  |  |  |  |  |  |  |  |  |  |  |                 ├> \\"http://a.ag#a\\"
+|  |  |  |  |  |  |  |  |  |  |  |                 |  └> …
+|  |  |  |  |  |  |  |  |  |  |  |                 └> …
+|  |  |  |  |  |  |  |  |  |  |  └> …
+|  |  |  |  |  |  |  |  |  |  └> …
+|  |  |  |  |  |  |  |  |  └> …
+|  |  |  |  |  |  |  |  └> …
+|  |  |  |  |  |  |  └> …
+|  |  |  |  |  |  └> …
+|  |  |  |  |  └> …
+|  |  |  |  └> …
+|  |  |  └> …
+|  |  └> …
+|  └> …
+└> …"
+`;
+
+exports[`webUrl (integration) should be able to shrink http://user:pass@my.domain.org/a/z 1`] = `
+"\\"http://user:pass@my.domain.org/a/z\\"
+├> \\"http://my.domain.org/a/z\\"
+|  ├> \\"http://domain.org/a/z\\"
+|  |  ├> \\"http://aomain.org/a/z\\"
+|  |  |  ├> \\"http://a.org/a/z\\"
+|  |  |  |  ├> \\"http://a.rg/a/z\\"
+|  |  |  |  |  ├> \\"http://a.ag/a/z\\"
+|  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  ├> \\"http://a.ag/z\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |     └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  ├> \\"http://a.ag/n\\"
+|  |  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  ├> \\"http://a.ag/h\\"
+|  |  |  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  ├> \\"http://a.ag/e\\"
+|  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag/c\\"
+|  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag/b\\"
+|  |  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |  |     |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  |     └> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  |        └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |  |           └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |  └> \\"http://a.ag/d\\"
+|  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |     ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |     |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |     └> \\"http://a.ag/c\\"
+|  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |        ├> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |  |        |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |        |     └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |        └> \\"http://a.ag/b\\"
+|  |  |  |  |  |  |  |  |  |           ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |           ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |           |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |           └> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |  |              ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  |              └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |  |                 └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |  └> \\"http://a.ag/g\\"
+|  |  |  |  |  |  |  |  |     ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |     ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |     |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |     └> \\"http://a.ag/f\\"
+|  |  |  |  |  |  |  |  |        ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |        ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |        |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |        └> \\"http://a.ag/e\\"
+|  |  |  |  |  |  |  |  |           ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |           |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           ├> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |           |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           |  └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |           |     └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           ├> \\"http://a.ag/c\\"
+|  |  |  |  |  |  |  |  |           |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           |  ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |           |  |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           |  └> \\"http://a.ag/b\\"
+|  |  |  |  |  |  |  |  |           |     ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           |     ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |           |     |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           |     └> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |           |        ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           |        └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |           |           └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |           └> \\"http://a.ag/d\\"
+|  |  |  |  |  |  |  |  |              ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |              ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |              |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |              └> \\"http://a.ag/c\\"
+|  |  |  |  |  |  |  |  |                 ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |                 ├> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |                 |  └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |                 ├> \\"http://a.ag/a\\"
+|  |  |  |  |  |  |  |  |                 |  ├> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |                 |  └> \\"http://a.ag/\\"
+|  |  |  |  |  |  |  |  |                 |     └> \\"http://a.ag\\"
+|  |  |  |  |  |  |  |  |                 └> …
+|  |  |  |  |  |  |  |  └> …
+|  |  |  |  |  |  |  └> …
+|  |  |  |  |  |  └> …
+|  |  |  |  |  └> …
+|  |  |  |  └> …
+|  |  |  └> …
+|  |  └> …
+|  └> …
+└> …"
+`;
diff --git a/test/unit/arbitrary/_internals/mappers/PartsToUrl.spec.ts b/test/unit/arbitrary/_internals/mappers/PartsToUrl.spec.ts
new file mode 100644
index 00000000..371025da
--- /dev/null
+++ b/test/unit/arbitrary/_internals/mappers/PartsToUrl.spec.ts
@@ -0,0 +1,46 @@
+import { URL } from 'url';
+import fc from '../../../../../lib/fast-check';
+import { partsToUrlUnmapper } from '../../../../../src/arbitrary/_internals/mappers/PartsToUrl';
+
+describe('partsToUrlUnmapper', () => {
+  it('should properly extract all parts of an url', () =>
+    fc.assert(
+      fc.property(
+        fc.webUrl({
+          authoritySettings: {
+            withIPv4: true,
+            withIPv4Extended: true,
+            withIPv6: true,
+            withPort: true,
+            withUserInfo: true,
+          },
+          withFragments: true,
+          withQueryParameters: true,
+        }),
+        (url) => {
+          // Arrange
+          const parsed = new URL(url);
+
+          // Act
+          const [scheme, authority, , query, fragments] = partsToUrlUnmapper(url);
+
+          // Assert
+          expect(scheme).toBe(url.split('://')[0]);
+          expect(decodeURIComponent(authority)).toContain(decodeURIComponent(parsed.username));
+          expect(decodeURIComponent(authority)).toContain(decodeURIComponent(parsed.password));
+          // Cannot assert on hostname: ips are sanitized
+          // Cannot assert on pathname: paths are sanitized (eg.: .., .)
+          if (parsed.search.length !== 0) {
+            expect(decodeURIComponent(query!)).toBe(decodeURIComponent(parsed.search.substring(1)));
+          } else {
+            expect([null, '']).toContain(query);
+          }
+          if (parsed.hash.length !== 0) {
+            expect(decodeURIComponent(fragments!)).toBe(decodeURIComponent(parsed.hash.substring(1)));
+          } else {
+            expect([null, '']).toContain(fragments);
+          }
+        }
+      )
+    ));
+});
diff --git a/test/unit/arbitrary/_internals/mappers/SegmentsToPath.spec.ts b/test/unit/arbitrary/_internals/mappers/SegmentsToPath.spec.ts
new file mode 100644
index 00000000..f7872243
--- /dev/null
+++ b/test/unit/arbitrary/_internals/mappers/SegmentsToPath.spec.ts
@@ -0,0 +1,21 @@
+import fc from '../../../../../lib/fast-check';
+import {
+  segmentsToPathMapper,
+  segmentsToPathUnmapper,
+} from '../../../../../src/arbitrary/_internals/mappers/SegmentsToPath';
+
+describe('segmentsToPathUnmapper', () => {
+  it('should be able to unmap any mapped value', () =>
+    fc.assert(
+      fc.property(fc.array(fc.webSegment()), (segments) => {
+        // Arrange
+        const mapped = segmentsToPathMapper(segments);
+
+        // Act
+        const out = segmentsToPathUnmapper(mapped);
+
+        // Assert
+        expect(out).toEqual(segments);
+      })
+    ));
+});
diff --git a/test/unit/arbitrary/webUrl.spec.ts b/test/unit/arbitrary/webUrl.spec.ts
new file mode 100644
index 00000000..44142df7
--- /dev/null
+++ b/test/unit/arbitrary/webUrl.spec.ts
@@ -0,0 +1,90 @@
+import fc from '../../../lib/fast-check';
+import { webUrl, WebUrlConstraints } from '../../../src/arbitrary/webUrl';
+import { convertToNext } from '../../../src/check/arbitrary/definition/Converters';
+import { URL } from 'url';
+
+import {
+  assertProduceCorrectValues,
+  assertProduceSameValueGivenSameSeed,
+  assertProduceValuesShrinkableWithoutContext,
+  assertShrinkProducesSameValueWithoutInitialContext,
+} from '../check/arbitrary/generic/NextArbitraryAssertions';
+import { NextValue } from '../../../src/check/arbitrary/definition/NextValue';
+import { buildNextShrinkTree, renderTree } from '../check/arbitrary/generic/ShrinkTree';
+
+function beforeEachHook() {
+  jest.resetModules();
+  jest.restoreAllMocks();
+  fc.configureGlobal({ beforeEach: beforeEachHook });
+}
+beforeEach(beforeEachHook);
+
+describe('webUrl (integration)', () => {
+  type Extra = WebUrlConstraints;
+  const extraParameters: fc.Arbitrary<Extra> = fc.record(
+    {
+      validSchemes: fc.constant(['ftp']),
+      authoritySettings: fc.record(
+        {
+          withIPv4: fc.boolean(),
+          withIPv6: fc.boolean(),
+          withIPv4Extended: fc.boolean(),
+          withUserInfo: fc.boolean(),
+          withPort: fc.boolean(),
+        },
+        { requiredKeys: [] }
+      ),
+      withQueryParameters: fc.boolean(),
+      withFragments: fc.boolean(),
+    },
+    { requiredKeys: [] }
+  );
+
+  const isCorrect = (t: string) => {
+    // Valid url given the specs defined by WHATWG URL Standard: https://url.spec.whatwg.org/
+    // A TypeError will be thrown if the input is not a valid URL: https://nodejs.org/api/url.html#url_constructor_new_url_input_base
+    expect(() => new URL(t)).not.toThrow();
+  };
+
+  const webUrlBuilder = (extra: Extra) => convertToNext(webUrl(extra));
+
+  it('should produce the same values given the same seed', () => {
+    assertProduceSameValueGivenSameSeed(webUrlBuilder, { extraParameters });
+  });
+
+  it('should only produce correct values', () => {
+    assertProduceCorrectValues(webUrlBuilder, isCorrect);
+  });
+
+  it('should produce values seen as shrinkable without any context', () => {
+    assertProduceValuesShrinkableWithoutContext(webUrlBuilder, { extraParameters });
+  });
+
+  it('should be able to shrink to the same values without initial context', () => {
+    assertShrinkProducesSameValueWithoutInitialContext(webUrlBuilder, { extraParameters });
+  });
+
+  it.each`
+    rawValue
+    ${'http://my.domain.org/a/z'}
+    ${'http://user:pass@my.domain.org/a/z'}
+    ${'http://my.domain.org/a/z?query#fragments'}
+  `('should be able to shrink $rawValue', ({ rawValue }) => {
+    // Arrange
+    const arb = convertToNext(
+      webUrl({
+        authoritySettings: { withUserInfo: true },
+        withQueryParameters: true,
+        withFragments: true,
+      })
+    );
+    const value = new NextValue(rawValue, undefined);
+
+    // Act
+    const renderedTree = renderTree(buildNextShrinkTree(arb, value, { numItems: 100 })).join('\n');
+
+    // Assert
+    expect(arb.canShrinkWithoutContext(rawValue)).toBe(true);
+    expect(renderedTree).toMatchSnapshot();
+  });
+});