Skip to content

Commit

Permalink
Fix up tests
Browse files Browse the repository at this point in the history
  • Loading branch information
PuruVJ committed Dec 27, 2024
1 parent da42af9 commit 4d5bb1c
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 120 deletions.
180 changes: 115 additions & 65 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function createDraggable({
delegate?: () => HTMLElement;
onError?: (error: ErrorInfo) => void;
} = {}) {
const instances = new WeakMap<HTMLElement | SVGElement, DraggableInstance>();
const instances = new Map<HTMLElement | SVGElement, DraggableInstance>();
let listeners_initialized = false;

/** track multiple active nodes by pointerId */
Expand Down Expand Up @@ -361,11 +361,88 @@ export function createDraggable({
return combined.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
}

function update_plugin(
function update(instance: DraggableInstance, new_plugins: Plugin[] = []): void {
// Early return if no plugins and instance has no plugins
if (!new_plugins.length && !instance.plugins.length) {
return;
}

// Check if instance is currently dragging or interacting
const is_active = instance.ctx.isDragging || instance.ctx.isInteracting;

// Initialize plugins only if needed
const new_plugin_list = is_active
? new_plugins.filter((p) => p.liveUpdate)
: initialize_plugins(new_plugins);

if (is_active) {
// Fast path for active instances
let has_changes = false;
const len = new_plugin_list.length;

// Use for loop for better performance
for (let i = 0; i < len; i++) {
const plugin = new_plugin_list[i];
const old_plugin = find_plugin_by_name(instance.plugins, plugin.name);

if (update_plugin_if_needed(instance, old_plugin, plugin)) {
has_changes = true;
}
}

// Only rerun drag if changes occurred and we have a last event
if (has_changes && instance.ctx.lastEvent && is_node_active(instance.root_node)) {
handle_pointer_move(instance.ctx.lastEvent);
}

return;
}

// Inactive instance path
let has_changes = false;

// Process removals first
if (instance.plugins.length > 0) {
const removed = find_removed_plugins(instance.plugins, new_plugin_list);
if (removed.length > 0) {
cleanup_plugins(instance, removed);
has_changes = true;
}
}

// Process updates and additions
const len = new_plugin_list.length;
for (let i = 0; i < len; i++) {
const plugin = new_plugin_list[i];
const old_plugin = find_plugin_by_name(instance.plugins, plugin.name);

if (update_plugin_if_needed(instance, old_plugin, plugin)) {
has_changes = true;
}
}

// Update instance plugins only if needed
if (has_changes) {
instance.plugins = new_plugin_list;
}
}

// Helper functions to improve readability and reusability
function find_plugin_by_name(plugins: Plugin[], name: string): Plugin | undefined {
const len = plugins.length;
for (let i = 0; i < len; i++) {
if (plugins[i].name === name) {
return plugins[i];
}
}
return undefined;
}

function update_plugin_if_needed(
instance: DraggableInstance,
old_plugin: Plugin | undefined,
new_plugin: Plugin,
) {
): boolean {
// Skip if same instance and not live-updateable
if (old_plugin === new_plugin && !new_plugin.liveUpdate) {
return false;
Expand All @@ -379,72 +456,51 @@ export function createDraggable({

// Setup new plugin
const state = new_plugin.setup?.(instance.ctx);
flush_effects(instance);
if (state) instance.states.set(new_plugin.name, state);
if (state) {
instance.states.set(new_plugin.name, state);
}

return true;
}

function update(instance: DraggableInstance, new_plugins: Plugin[] = []) {
const old_plugin_map = new Map(instance.plugins.map((p) => [p.name, p]));
const new_plugin_list = initialize_plugins(new_plugins);
let has_changes = false;
function find_removed_plugins(old_plugins: Plugin[], new_plugins: Plugin[]): Plugin[] {
return old_plugins.filter((p) => !new_plugins.some((np) => np.name === p.name));
}

// Check if this instance is currently involved in any drag operation
let is_active = false;
for (const node of active_nodes.values()) {
if (node === instance.root_node) {
is_active = true;
break;
}
function cleanup_plugins(instance: DraggableInstance, plugins: Plugin[]): void {
const len = plugins.length;
for (let i = 0; i < len; i++) {
const plugin = plugins[i];
plugin.cleanup?.(instance.ctx, instance.states.get(plugin.name));
instance.states.delete(plugin.name);
}
}

// During drag, only update plugins that opted into live updates
if (is_active && (instance.ctx.isDragging || instance.ctx.isInteracting)) {
const updated_plugins = new_plugin_list.filter((plugin) => plugin.liveUpdate);

for (const plugin of updated_plugins) {
const old_plugin = old_plugin_map.get(plugin.name);
if (update_plugin(instance, old_plugin, plugin)) {
has_changes = true;
}
}
function is_node_active(node: HTMLElement | SVGElement): boolean {
for (const activeNode of active_nodes.values()) {
if (activeNode === node) return true;
}
return false;
}

// If we made changes and we're the active node, re-run drag
if (has_changes) {
for (const node of active_nodes.values()) {
if (node === instance.root_node && instance.ctx.lastEvent) {
handle_pointer_move(instance.ctx.lastEvent);
break;
}
}
function destroy(instance: DraggableInstance) {
for (const [pointer_id, active_node] of active_nodes) {
if (active_node === instance.root_node) {
cleanup_active_node(pointer_id);
}

return;
}

// Clean up removed plugins
const removed_plugins = instance.plugins.filter(
(p) => !new_plugin_list.some((np) => np.name === p.name),
);

for (const plugin of removed_plugins) {
for (const plugin of instance.plugins) {
plugin.cleanup?.(instance.ctx, instance.states.get(plugin.name));
instance.states.delete(plugin.name);
has_changes = true;
}

// Update or setup new plugins
for (const plugin of new_plugin_list) {
const old_plugin = old_plugin_map.get(plugin.name);
if (update_plugin(instance, old_plugin, plugin)) {
has_changes = true;
}
}
instances.delete(instance.root_node);
}

// Update instance plugins list if there were changes
if (has_changes) {
instance.plugins = new_plugin_list;
// Dispose all draggable instances. Can't be recreated again
function dispose() {
for (const instance of instances.values()) {
destroy(instance);
}
}

Expand Down Expand Up @@ -535,19 +591,13 @@ export function createDraggable({
update: (newOptions: Plugin[]) => update(instance, newOptions),

destroy() {
for (const [pointer_id, active_node] of active_nodes) {
if (active_node === node) {
cleanup_active_node(pointer_id);
}
}

for (const plugin of instance.plugins) {
plugin.cleanup?.(instance.ctx, instance.states.get(plugin.name));
}

instances.delete(node);
destroy(instance);
},
};
},

dispose,

[Symbol.dispose]: dispose,
};
}
7 changes: 5 additions & 2 deletions packages/core/src/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export function unstable_definePlugin<ArgsTuple extends any[], State = void>(
let memoized_plugin: Plugin<State> & { args: ArgsTuple };

return (...args: ArgsTuple): Plugin<State> & { args: ArgsTuple } => {
const final_args = args.length ? args : defaultArgs ?? ([] as unknown as ArgsTuple);
const final_args = args.length ? args : (defaultArgs ?? ([] as unknown as ArgsTuple));

if (memoized_plugin && last_args === final_args) {
return memoized_plugin;
Expand Down Expand Up @@ -201,10 +201,13 @@ export const stateMarker = unstable_definePlugin({
});

// Degree of Freedom X and Y
export const axis = unstable_definePlugin<[value: 'x' | 'y']>({
export const axis = unstable_definePlugin<[value?: 'x' | 'y']>({
name: 'neodrag:axis',

drag([value], ctx) {
// Let dragging go on if axis is undefined
if (!value) return;

ctx.propose(value === 'x' ? ctx.proposed.x : null, value === 'y' ? ctx.proposed.y : null);
},
});
Expand Down
18 changes: 7 additions & 11 deletions packages/core/test-app/e2e/defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ test.describe('defaults', () => {

const div = page.getByTestId('draggable');

await expect(div).toHaveClass(/neodrag/);
await expect(div).not.toHaveClass('neodrag-dragging');
await expect(div).not.toHaveClass('neodrag-dragged');
await expect(div).toHaveCSS('translate', '0px');
await expect(div).toHaveAttribute('data-neodrag-state', 'idle');
await expect(div).toHaveAttribute('data-neodrag', '');
await expect(div).toHaveCSS('translate', 'none');
});

test('should be dragged by mouse', async ({ page, isMobile }) => {
Expand All @@ -24,14 +23,12 @@ test.describe('defaults', () => {
const { x, y } = await get_mouse_position(page);
await page.mouse.down();
await page.mouse.move(x + 100, y + 100);
await expect(div).toHaveClass(/neodrag neodrag-dragging/);
await expect(div).toHaveAttribute('data-neodrag-state', 'dragging');
await page.mouse.up();

// Make sure neodrag-dragging isn't there anymore
await expect(div).not.toHaveClass('neodrag-dragging');
await expect(div).toHaveAttribute('data-neodrag-state', 'idle');

expect(div).toHaveCSS('translate', '100px 100px');
expect(div).toHaveClass(/neodrag neodrag-dragged/);
});

test('should be dragged by touch', async ({ page, isMobile }) => {
Expand All @@ -45,13 +42,12 @@ test.describe('defaults', () => {
const { x, y } = await get_mouse_position(page);
await page.mouse.down();
await page.mouse.move(x + 100, y + 100);
await expect(div).toHaveClass(/neodrag neodrag-dragging/);
await expect(div).toHaveAttribute('data-neodrag-state', 'dragging');
await page.mouse.up();

// Make sure neodrag-dragging isn't there anymore
await expect(div).not.toHaveClass('neodrag-dragging');
await expect(div).toHaveAttribute('data-neodrag-state', 'idle');

expect(div).toHaveCSS('translate', '100px 100px');
expect(div).toHaveClass(/neodrag neodrag-dragged/);
});
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,13 @@
import test, { expect } from '@playwright/test';
import { get_mouse_position, setup } from '../test-utils';
import { stringify } from 'devalue';
import { z } from 'zod';

test.describe('options.axis', () => {
test('move on both axes', async ({ page }) => {
await setup(page, 'options/axis', { axis: 'both' });

const div = page.getByTestId('draggable');

await div.hover();
const { x, y } = await get_mouse_position(page);
await page.mouse.down();
await page.mouse.move(x + 100, y + 100);
await page.mouse.up();

// Only X should have changed
await expect(div).toHaveCSS('translate', '100px 100px');
});
const schema = z.enum(['x', 'y']).optional();

test.describe('options.axis', () => {
test('move only on x-axis', async ({ page }) => {
await setup(page, 'options/axis', { axis: 'x' });
await setup(page, 'plugins/axis', schema, 'x');

const div = page.getByTestId('draggable');

Expand All @@ -33,7 +22,7 @@ test.describe('options.axis', () => {
});

test('move only on y-axis', async ({ page }) => {
await setup(page, 'options/axis', { axis: 'y' });
await setup(page, 'plugins/axis', schema, 'y');

const div = page.getByTestId('draggable');

Expand All @@ -47,23 +36,8 @@ test.describe('options.axis', () => {
await expect(div).toHaveCSS('translate', '0px 100px');
});

test("none: shouldn't move", async ({ page }) => {
await setup(page, 'options/axis', { axis: 'none' });

const div = page.getByTestId('draggable');

await div.hover();
const { x, y } = await get_mouse_position(page);
await page.mouse.down();
await page.mouse.move(x + 100, y + 100);
await page.mouse.up();

// Only X should have changed
await expect(div).toHaveCSS('translate', '0px');
});

test('undefined should default to both', async ({ page }) => {
await setup(page, 'options/axis', { axis: undefined });
test('undefined disables the plugin', async ({ page }) => {
await setup(page, 'plugins/axis', schema, undefined);

const div = page.getByTestId('draggable');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import test from '@playwright/test';
import { setup } from '../test-utils';
import { z } from 'zod';

test.describe('options.bounds', () => {
test.describe('coords', () => {
test('starts within bounds, fully overlapped', async ({ page }) => {
await setup(page, 'options/bounds/coords', {
bounds: { left: 0, top: 0, right: },
await setup(page, 'plugins/bounds/coords', z.any(), {
// bounds: { left: 0, top: 0, right: },
});
});
});
Expand Down
Loading

0 comments on commit 4d5bb1c

Please sign in to comment.