maybe tests are cool after all, fixed bugs and simplified deepDiff
This commit is contained in:
parent
441d7e383c
commit
cdbb11b14a
13 changed files with 585 additions and 132 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
|
|
||||||
dist
|
dist
|
||||||
|
.coverage
|
||||||
.solid
|
.solid
|
||||||
.output
|
.output
|
||||||
.vercel
|
.vercel
|
||||||
|
|
41
.vscode/launch.json
vendored
Normal file
41
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "bun",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug Bun",
|
||||||
|
// The path to a JavaScript or TypeScript file to run.
|
||||||
|
"program": "${file}",
|
||||||
|
// The arguments to pass to the program, if any.
|
||||||
|
"args": [],
|
||||||
|
// The working directory of the program.
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// The environment variables to pass to the program.
|
||||||
|
"env": {},
|
||||||
|
// If the environment variables should not be inherited from the parent process.
|
||||||
|
"strictEnv": false,
|
||||||
|
// If the program should be run in watch mode.
|
||||||
|
// This is equivalent to passing `--watch` to the `bun` executable.
|
||||||
|
// You can also set this to "hot" to enable hot reloading using `--hot`.
|
||||||
|
"watchMode": false,
|
||||||
|
// If the debugger should stop on the first line of the program.
|
||||||
|
"stopOnEntry": false,
|
||||||
|
// If the debugger should be disabled. (for example, breakpoints will not be hit)
|
||||||
|
"noDebug": false,
|
||||||
|
// The path to the `bun` executable, defaults to your `PATH` environment variable.
|
||||||
|
"runtime": "bun",
|
||||||
|
// The arguments to pass to the `bun` executable, if any.
|
||||||
|
// Unlike `args`, these are passed to the executable itself, not the program.
|
||||||
|
"runtimeArgs": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "bun",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Attach to Bun",
|
||||||
|
// The URL of the WebSocket inspector to attach to.
|
||||||
|
// This value can be retreived by using `bun --inspect`.
|
||||||
|
"url": "ws://localhost:6499/",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
// The path to the `bun` executable.
|
||||||
|
"bun.runtime": "/path/to/bun",
|
||||||
|
"bun.debugTerminal": {
|
||||||
|
// If support for Bun should be added to the default "JavaScript Debug Terminal".
|
||||||
|
"enabled": true,
|
||||||
|
// If the debugger should stop on the first line of the program.
|
||||||
|
"stopOnEntry": false,
|
||||||
|
}
|
||||||
|
}
|
118
app.config.ts
118
app.config.ts
|
@ -7,69 +7,64 @@ export default defineConfig({
|
||||||
cspNonce: 'KAAS_IS_AWESOME',
|
cspNonce: 'KAAS_IS_AWESOME',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
VitePWA({
|
// VitePWA({
|
||||||
strategies: 'injectManifest',
|
// strategies: 'injectManifest',
|
||||||
mode: 'development',
|
// registerType: 'autoUpdate',
|
||||||
|
// injectRegister: false,
|
||||||
|
|
||||||
registerType: 'autoUpdate',
|
// workbox: {
|
||||||
injectRegister: false,
|
// globPatterns: ['**/*.{js,css,html,svg,png,svg,ico}'],
|
||||||
|
// cleanupOutdatedCaches: true,
|
||||||
|
// clientsClaim: true,
|
||||||
|
// },
|
||||||
|
// injectManifest: {
|
||||||
|
// globPatterns: ['**/*.{js,css,html,svg,png,svg,ico}'],
|
||||||
|
// },
|
||||||
|
|
||||||
// pwaAssets: { disabled: false, config: true, htmlPreset: '2023', overrideManifestIcons: true },
|
// manifest: {
|
||||||
workbox: {
|
// name: 'Calque - manage your i18n files',
|
||||||
globPatterns: ['**/*.{js,css,html,svg,png,svg,ico}'],
|
// short_name: 'KAAS',
|
||||||
cleanupOutdatedCaches: true,
|
// description: 'Simple tool for maitaining i18n files',
|
||||||
clientsClaim: true,
|
// icons: [
|
||||||
},
|
// {
|
||||||
injectManifest: {
|
// src: '/images/favicon.dark.svg',
|
||||||
globPatterns: ['**/*.{js,css,html,svg,png,svg,ico}'],
|
// type: 'image/svg+xml',
|
||||||
},
|
// sizes: 'any'
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// display_override: ['window-controls-overlay'],
|
||||||
|
// screenshots: [
|
||||||
|
// {
|
||||||
|
// src: '/images/screenshots/narrow.png',
|
||||||
|
// type: 'image/png',
|
||||||
|
// sizes: '538x1133',
|
||||||
|
// form_factor: 'narrow'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// src: '/images/screenshots/wide.png',
|
||||||
|
// type: 'image/png',
|
||||||
|
// sizes: '2092x1295',
|
||||||
|
// form_factor: 'wide'
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// file_handlers: [
|
||||||
|
// {
|
||||||
|
// action: '/edit',
|
||||||
|
// accept: {
|
||||||
|
// 'text/*': [
|
||||||
|
// '.json'
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// },
|
||||||
|
|
||||||
base: '/',
|
// devOptions: {
|
||||||
manifest: {
|
// enabled: true,
|
||||||
name: 'Calque - manage your i18n files',
|
// type: 'module',
|
||||||
short_name: 'KAAS',
|
// navigateFallback: 'index.html',
|
||||||
description: 'Simple tool for maitaining i18n files',
|
// },
|
||||||
icons: [
|
// }),
|
||||||
{
|
|
||||||
src: '/images/favicon.dark.svg',
|
|
||||||
type: 'image/svg+xml',
|
|
||||||
sizes: 'any'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
display_override: ['window-controls-overlay'],
|
|
||||||
screenshots: [
|
|
||||||
{
|
|
||||||
src: '/images/screenshots/narrow.png',
|
|
||||||
type: 'image/png',
|
|
||||||
sizes: '538x1133',
|
|
||||||
form_factor: 'narrow'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: '/images/screenshots/wide.png',
|
|
||||||
type: 'image/png',
|
|
||||||
sizes: '2092x1295',
|
|
||||||
form_factor: 'wide'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
file_handlers: [
|
|
||||||
{
|
|
||||||
action: '/edit',
|
|
||||||
accept: {
|
|
||||||
'text/*': [
|
|
||||||
'.json'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
devOptions: {
|
|
||||||
enabled: true,
|
|
||||||
type: 'module',
|
|
||||||
navigateFallback: 'index.html',
|
|
||||||
resolveTempFolder: () => './.output/public',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
solid: {
|
solid: {
|
||||||
|
@ -81,8 +76,5 @@ export default defineConfig({
|
||||||
prerender: {
|
prerender: {
|
||||||
crawlLinks: true,
|
crawlLinks: true,
|
||||||
},
|
},
|
||||||
routeRules: {
|
|
||||||
'/manifest.json': { static: true }
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
5
bunfig.toml
Normal file
5
bunfig.toml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[test]
|
||||||
|
coverage = true
|
||||||
|
coverageReporter = ['text', 'lcov']
|
||||||
|
coverageDir = './.coverage'
|
||||||
|
preload = "./test.config.ts"
|
|
@ -18,18 +18,20 @@
|
||||||
"dev": "vinxi dev",
|
"dev": "vinxi dev",
|
||||||
"build": "vinxi build",
|
"build": "vinxi build",
|
||||||
"start": "vinxi start",
|
"start": "vinxi start",
|
||||||
"version": "vinxi version",
|
"version": "vinxi version"
|
||||||
"test": "vitest"
|
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@happy-dom/global-registrator": "^15.11.0",
|
||||||
|
"@sinonjs/fake-timers": "^13.0.5",
|
||||||
"@solidjs/testing-library": "^0.8.10",
|
"@solidjs/testing-library": "^0.8.10",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
|
"@types/sinonjs__fake-timers": "^8.1.5",
|
||||||
"@types/wicg-file-system-access": "^2023.10.5",
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
|
"bun-types": "^1.1.34",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
"vite-plugin-pwa": "^0.20.5",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vitest": "^2.1.4",
|
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
11
src/components/colorschemepicker.spec.tsx
Normal file
11
src/components/colorschemepicker.spec.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
import { render } from "@solidjs/testing-library"
|
||||||
|
import { ColorSchemePicker } from "./colorschemepicker";
|
||||||
|
|
||||||
|
// describe('<ColorSchemePicker />', () => {
|
||||||
|
// it('should render', async () => {
|
||||||
|
// render(() => <ColorSchemePicker />);
|
||||||
|
|
||||||
|
// expect(true).toBe(true);
|
||||||
|
// });
|
||||||
|
// });
|
|
@ -1,22 +1,430 @@
|
||||||
import { expect, describe, it, beforeEach, vi } from "vitest"
|
import { describe, beforeEach, it, expect, mock, afterAll, spyOn } from 'bun:test';
|
||||||
import { debounce } from "./utilities"
|
import { debounce, deepCopy, deepDiff, filter, map, MutarionKind, splitAt } from './utilities';
|
||||||
|
import { install } from '@sinonjs/fake-timers';
|
||||||
|
|
||||||
|
type MilliSeconds = number;
|
||||||
|
const useFakeTimers = () => {
|
||||||
|
const clock = install();
|
||||||
|
|
||||||
|
beforeEach(() => clock.reset());
|
||||||
|
afterAll(() => clock.uninstall());
|
||||||
|
|
||||||
|
return {
|
||||||
|
tick(timeToAdvance: MilliSeconds) {
|
||||||
|
clock.tick(timeToAdvance);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const first = <T>(iterable: Iterable<T>): T | undefined => {
|
||||||
|
for (const value of iterable) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('splitAt', () => {
|
||||||
|
it('should split the given string at the given index', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given = 'this.is.some.concatenated.string';
|
||||||
|
const expected = [
|
||||||
|
'this.is.some.concatenated',
|
||||||
|
'string',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const [a, b] = splitAt(given, given.lastIndexOf('.'));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(a).toBe(expected[0]);
|
||||||
|
expect(b).toBe(expected[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty second result when the index is negative', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given = 'this.is.some.concatenated.string';
|
||||||
|
const expected = [
|
||||||
|
'this.is.some.concatenated.string',
|
||||||
|
'',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const [a, b] = splitAt(given, -1);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(a).toBe(expected[0]);
|
||||||
|
expect(b).toBe(expected[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty second result when the index is larger then subject length', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given = 'this.is.some.concatenated.string';
|
||||||
|
const expected = [
|
||||||
|
'this.is.some.concatenated.string',
|
||||||
|
'',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const [a, b] = splitAt(given, given.length * 2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(a).toBe(expected[0]);
|
||||||
|
expect(b).toBe(expected[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('debounce', () => {
|
describe('debounce', () => {
|
||||||
beforeEach(() => {
|
const { tick } = useFakeTimers();
|
||||||
vi.useFakeTimers();
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should run the given callback after the provided time', async () => {
|
it('should run the given callback after the provided time', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const callback = vi.fn(() => { });
|
const callback = mock(() => { });
|
||||||
const delay = 1000;
|
const delay = 1000;
|
||||||
const debounced = debounce(callback, delay);
|
const debounced = debounce(callback, delay);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
debounced();
|
debounced();
|
||||||
vi.runAllTimers();
|
tick(delay);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(callback).toHaveBeenCalled();
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
it('should reset if another call is made', async () => {
|
||||||
|
// Arrange
|
||||||
|
const callback = mock(() => { });
|
||||||
|
const delay = 1000;
|
||||||
|
const debounced = debounce(callback, delay);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
debounced();
|
||||||
|
tick(delay / 2);
|
||||||
|
debounced();
|
||||||
|
tick(delay);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deepCopy', () => {
|
||||||
|
it('can skip values passed by reference (non-objects, null, and undefined)', async () => {
|
||||||
|
// arrange
|
||||||
|
const given = 'some string';
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepCopy(given);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toBe(given);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a value that does not point to same memory', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given = {};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepCopy(given);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(actual).not.toBe(given);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Date types', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given = new Date();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepCopy(given);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(actual).not.toBe(given);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Arrays', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given: any[] = [];
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepCopy(given);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(actual).not.toBe(given);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Sets', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given = new Set();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepCopy(given);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(actual).not.toBe(given);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Maps', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given = new Map();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepCopy(given);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(actual).not.toBe(given);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a value that does not point to same memory for nested properties', async () => {
|
||||||
|
// Arrange
|
||||||
|
const given = {
|
||||||
|
some: {
|
||||||
|
deep: {
|
||||||
|
value: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepCopy(given);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(actual.some.deep.value).not.toBe(given.some.deep.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deepDiff', () => {
|
||||||
|
it('should immedietly return when either `a` is not iterable', async () => {
|
||||||
|
// arrange
|
||||||
|
const a: any = 0;
|
||||||
|
const b = {};
|
||||||
|
const spy = spyOn(console, 'error').mockReturnValue(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([]);
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should immedietly return when either `b` is not iterable', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = {};
|
||||||
|
const b: any = 0;
|
||||||
|
const spy = spyOn(console, 'error').mockReturnValue(undefined);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([]);
|
||||||
|
expect(spy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should yield no results when both a and b are empty', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = {};
|
||||||
|
const b = {};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should yield no results when both a and b are equal', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key: 'value' };
|
||||||
|
const b = { key: 'value' };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should yield a mutation of type create when `b` contains a key that `a` does not', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = {};
|
||||||
|
const b = { key: 'value' };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = first(deepDiff(a, b));
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual({ kind: MutarionKind.Create, key: 'key', value: 'value' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should yield a mutation of type delete when `a` contains a key that `b` does not', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key: 'value' };
|
||||||
|
const b = {};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = first(deepDiff(a, b));
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual({ kind: MutarionKind.Delete, key: 'key' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should yield a mutation of type update when the value of a key in `a` is not equal to the value of the same key in `b`', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key: 'old' };
|
||||||
|
const b = { key: 'new' };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = first(deepDiff(a, b));
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual({ kind: MutarionKind.Update, key: 'key', original: 'old', value: 'new' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should iterate over nested values', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { some: { nested: { key: 'old' } } };
|
||||||
|
const b = { some: { nested: { key: 'new' } } };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([{ kind: MutarionKind.Update, key: 'some.nested.key', original: 'old', value: 'new' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deleted keys', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key1: 'value1', key2: 'value2', key3: 'value3', key4: 'value4', key5: 'value5' };
|
||||||
|
const b = { key1: 'value1', key4: 'value4', key5: 'value5' };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([
|
||||||
|
{ kind: MutarionKind.Delete, key: 'key2' },
|
||||||
|
{ kind: MutarionKind.Delete, key: 'key3' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle created keys', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key1: 'value1', key4: 'value4', key5: 'value5' };
|
||||||
|
const b = { key1: 'value1', key2: 'value2', key3: 'value3', key4: 'value4', key5: 'value5' };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([
|
||||||
|
{ kind: MutarionKind.Create, key: 'key2', value: 'value2' },
|
||||||
|
{ kind: MutarionKind.Create, key: 'key3', value: 'value3' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle renamed keys', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key1: 'value1', key2_old: 'value2', key3: 'value3' };
|
||||||
|
const b = { key1: 'value1', key2_new: 'value2', key3: 'value3', };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([
|
||||||
|
{ kind: MutarionKind.Delete, key: 'key2_old' },
|
||||||
|
{ kind: MutarionKind.Create, key: 'key2_new', value: 'value2' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle `Array` values', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key: [1] };
|
||||||
|
const b = { key: [2] };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([
|
||||||
|
{ kind: MutarionKind.Update, key: 'key.0', original: 1, value: 2 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle `Set` values', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key: new Set([1, 2, 3]) };
|
||||||
|
const b = { key: new Set([1, 5, 3]) };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([
|
||||||
|
{ kind: MutarionKind.Delete, key: 'key.2' },
|
||||||
|
{ kind: MutarionKind.Create, key: 'key.5', value: 5 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle `Map` values', async () => {
|
||||||
|
// arrange
|
||||||
|
const a = { key: new Map([['key', 'old']]) };
|
||||||
|
const b = { key: new Map([['key', 'new']]) };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = deepDiff(a, b).toArray();
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([
|
||||||
|
{ kind: MutarionKind.Update, key: 'key.key', original: 'old', value: 'new' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter', () => {
|
||||||
|
it('should yield a value when the predicate returns true', async () => {
|
||||||
|
// arrange
|
||||||
|
const generator = async function* () {
|
||||||
|
for (const i of new Array(10).fill('').map((_, i) => i)) {
|
||||||
|
yield i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const predicate = (i: number) => i % 2 === 0;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = await Array.fromAsync(filter(generator(), predicate as any));
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([0, 2, 4, 6, 8]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('map', () => {
|
||||||
|
const generator = async function* () {
|
||||||
|
for (const i of new Array(10).fill('').map((_, i) => i)) {
|
||||||
|
yield i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should yield a value when the predicate returns true', async () => {
|
||||||
|
// arrange
|
||||||
|
const mapFn = (i: number) => `nr ${i}`;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const actual = await Array.fromAsync(map(generator(), mapFn));
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
expect(actual).toEqual([
|
||||||
|
'nr 0',
|
||||||
|
'nr 1',
|
||||||
|
'nr 2',
|
||||||
|
'nr 3',
|
||||||
|
'nr 4',
|
||||||
|
'nr 5',
|
||||||
|
'nr 6',
|
||||||
|
'nr 7',
|
||||||
|
'nr 8',
|
||||||
|
'nr 9',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
export const splitAt = (subject: string, index: number): readonly [string, string] => {
|
export const splitAt = (subject: string, index: number): readonly [string, string] => {
|
||||||
return [subject.slice(0, index), subject.slice(index + 1)] as const;
|
if (index < 0) {
|
||||||
|
return [subject, ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > subject.length) {
|
||||||
|
return [subject, ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [subject.slice(0, index), subject.slice(index + 1)];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const debounce = <T extends (...args: any[]) => void>(callback: T, delay: number): ((...args: Parameters<T>) => void) => {
|
export const debounce = <T extends (...args: any[]) => void>(callback: T, delay: number): ((...args: Parameters<T>) => void) => {
|
||||||
|
@ -53,23 +61,19 @@ export type Mutation = { key: string } & (Created | Updated | Deleted);
|
||||||
|
|
||||||
export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, path: string[] = []): Generator<Mutation, void, unknown> {
|
export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, path: string[] = []): Generator<Mutation, void, unknown> {
|
||||||
if (!isIterable(a) || !isIterable(b)) {
|
if (!isIterable(a) || !isIterable(b)) {
|
||||||
console.log('Edge cases', a, b);
|
console.error('Edge cases', a, b);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [[keyA, valueA], [keyB, valueB]] of zip(entriesOf(a), entriesOf(b))) {
|
for (const [[keyA, valueA], [keyB, valueB]] of zip(entriesOf(a), entriesOf(b))) {
|
||||||
if (!keyA && !keyB) {
|
if (keyA === undefined && keyB) {
|
||||||
throw new Error('this code should not be reachable, there is a bug with an unhandled/unknown edge case');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!keyA && keyB) {
|
|
||||||
yield { key: path.concat(keyB.toString()).join('.'), kind: MutarionKind.Create, value: valueB };
|
yield { key: path.concat(keyB.toString()).join('.'), kind: MutarionKind.Create, value: valueB };
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyA && !keyB) {
|
if (keyA && keyB === undefined) {
|
||||||
yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete };
|
yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete };
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
|
@ -112,40 +116,42 @@ const entriesOf = (subject: object): Iterable<readonly [string | number, any]> =
|
||||||
|
|
||||||
return Object.entries(subject);
|
return Object.entries(subject);
|
||||||
};
|
};
|
||||||
const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable<readonly [string | number, any]>): Generator<readonly [readonly [string | number | undefined, any], readonly [string | number | undefined, any]], void, unknown> {
|
|
||||||
|
type ZippedPair =
|
||||||
|
| readonly [readonly [string | number, any], readonly [string | number, any]]
|
||||||
|
| readonly [readonly [undefined, undefined], readonly [string | number, any]]
|
||||||
|
| readonly [readonly [string | number, any], readonly [undefined, undefined]]
|
||||||
|
;
|
||||||
|
const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable<readonly [string | number, any]>): Generator<ZippedPair, void, unknown> {
|
||||||
const iterA = bufferredIterator(a);
|
const iterA = bufferredIterator(a);
|
||||||
const iterB = bufferredIterator(b);
|
const iterB = bufferredIterator(b);
|
||||||
|
|
||||||
const EMPTY = [undefined, undefined] as [string | number | undefined, any];
|
const EMPTY = [undefined, undefined] as const;
|
||||||
|
|
||||||
while (!iterA.done || !iterB.done) {
|
while (!iterA.done || !iterB.done) {
|
||||||
// if we have a match on the keys of a and b we can simply consume and yield
|
// if we have a match on the keys of a and b we can simply consume and yield
|
||||||
if (iterA.current.key === iterB.current.key) {
|
if (iterA.current.key === iterB.current.key) {
|
||||||
|
// When we match keys it could have happened that
|
||||||
|
// there are as many keys added as there are deleted,
|
||||||
|
// therefor we can now flush both a and b because we are aligned
|
||||||
|
yield* iterA.flush().map(entry => [entry, EMPTY] as const);
|
||||||
|
yield* iterB.flush().map(entry => [EMPTY, entry] as const);
|
||||||
|
|
||||||
yield [iterA.consume(), iterB.consume()];
|
yield [iterA.consume(), iterB.consume()];
|
||||||
}
|
}
|
||||||
|
|
||||||
// key of a aligns with last key in buffer b
|
// key of a aligns with last key in buffer b
|
||||||
// conclusion: a has key(s) that b does not
|
// conclusion: a has key(s) that b does not
|
||||||
else if (iterA.current.key === iterB.top.key) {
|
else if (iterA.current.key === iterB.top.key) {
|
||||||
const a = iterA.pop()!;
|
yield* iterA.flush().map(entry => [entry, EMPTY] as const);
|
||||||
|
yield [iterA.consume(), iterB.consume()];
|
||||||
for (const [key, value] of iterA.flush()) {
|
|
||||||
yield [[key, value], EMPTY];
|
|
||||||
}
|
|
||||||
|
|
||||||
yield [a, iterB.consume()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the reverse case, key of b is aligns with the last key in buffer a
|
// the reverse case, key of b is aligns with the last key in buffer a
|
||||||
// conclusion: a is missing key(s) the b does have
|
// conclusion: a is missing key(s) the b does have
|
||||||
else if (iterB.current.key === iterA.top.key) {
|
else if (iterB.current.key === iterA.top.key) {
|
||||||
const b = iterB.pop()!;
|
yield* iterB.flush().map(entry => [EMPTY, entry] as const);
|
||||||
|
yield [iterA.consume(), iterB.consume()];
|
||||||
for (const [key, value] of iterB.flush()) {
|
|
||||||
yield [EMPTY, [key, value]];
|
|
||||||
}
|
|
||||||
|
|
||||||
yield [iterA.consume(), b];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (iterA.done && !iterB.done) {
|
else if (iterA.done && !iterB.done) {
|
||||||
|
@ -168,7 +174,6 @@ const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable
|
||||||
const bufferredIterator = <T extends readonly [string | number, any]>(subject: Iterable<T>) => {
|
const bufferredIterator = <T extends readonly [string | number, any]>(subject: Iterable<T>) => {
|
||||||
const iterator = Iterator.from(subject);
|
const iterator = Iterator.from(subject);
|
||||||
const buffer: T[] = [];
|
const buffer: T[] = [];
|
||||||
let cursor: number = 0;
|
|
||||||
let done = false;
|
let done = false;
|
||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
|
@ -176,7 +181,7 @@ const bufferredIterator = <T extends readonly [string | number, any]>(subject: I
|
||||||
done = res.done ?? false;
|
done = res.done ?? false;
|
||||||
|
|
||||||
if (!done) {
|
if (!done) {
|
||||||
cursor = buffer.push(res.value) - 1;
|
buffer.push(res.value)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -184,16 +189,10 @@ const bufferredIterator = <T extends readonly [string | number, any]>(subject: I
|
||||||
|
|
||||||
return {
|
return {
|
||||||
advance() {
|
advance() {
|
||||||
if (buffer.length > 0 && cursor < (buffer.length - 1)) {
|
next();
|
||||||
cursor++;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
consume() {
|
consume() {
|
||||||
cursor = 0;
|
|
||||||
const value = buffer.shift()!;
|
const value = buffer.shift()!;
|
||||||
|
|
||||||
this.advance();
|
this.advance();
|
||||||
|
@ -202,15 +201,9 @@ const bufferredIterator = <T extends readonly [string | number, any]>(subject: I
|
||||||
},
|
},
|
||||||
|
|
||||||
flush(): T[] {
|
flush(): T[] {
|
||||||
cursor = 0;
|
const entries = buffer.splice(0, buffer.length - 1);
|
||||||
|
|
||||||
return buffer.splice(0, buffer.length);
|
return entries;
|
||||||
},
|
|
||||||
|
|
||||||
pop() {
|
|
||||||
cursor--;
|
|
||||||
|
|
||||||
return buffer.pop();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
get done() {
|
get done() {
|
||||||
|
@ -224,14 +217,10 @@ const bufferredIterator = <T extends readonly [string | number, any]>(subject: I
|
||||||
},
|
},
|
||||||
|
|
||||||
get current() {
|
get current() {
|
||||||
const [key = undefined, value = undefined] = buffer.at(cursor) ?? [];
|
const [key = undefined, value = undefined] = buffer.at(-1) ?? [];
|
||||||
|
|
||||||
return { key, value };
|
return { key, value };
|
||||||
},
|
},
|
||||||
|
|
||||||
get entry() {
|
|
||||||
return [this.current.key, this.current.value] as const;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
3
test.config.ts
Normal file
3
test.config.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
||||||
|
|
||||||
|
GlobalRegistrator.register();
|
|
@ -15,7 +15,8 @@
|
||||||
"@testing-library/jest-dom",
|
"@testing-library/jest-dom",
|
||||||
"@types/wicg-file-system-access",
|
"@types/wicg-file-system-access",
|
||||||
"vinxi/types/client",
|
"vinxi/types/client",
|
||||||
"vite-plugin-pwa/solid"
|
"vite-plugin-pwa/solid",
|
||||||
|
"bun-types"
|
||||||
],
|
],
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
import solid from "vite-plugin-solid"
|
|
||||||
import { defineConfig } from "vitest/config"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [solid()],
|
|
||||||
root: './src',
|
|
||||||
resolve: {
|
|
||||||
conditions: ["development", "browser"],
|
|
||||||
},
|
|
||||||
})
|
|
Loading…
Add table
Add a link
Reference in a new issue