This commit is contained in:
2025-12-18 14:26:17 +01:00
parent 61f675d991
commit bf1f09c774
9 changed files with 387 additions and 30 deletions

View File

@@ -0,0 +1,23 @@
/**
* Abstract base class for all Blockers.
*
* Blockers are frontend mechanisms that prevent actions to improve UX.
* They are not security mechanisms and must be reversible and best-effort.
*/
export abstract class Blocker {
/**
* Checks if the action can be executed.
* @returns true if the action can proceed, false otherwise.
*/
abstract canExecute(): boolean;
/**
* Blocks the action from being executed.
*/
abstract block(): void;
/**
* Releases the block, allowing future executions.
*/
abstract release(): void;
}

View File

@@ -0,0 +1,37 @@
import { describe, it, expect } from 'vitest';
import { SubmitBlocker } from './SubmitBlocker';
describe('SubmitBlocker', () => {
let blocker: SubmitBlocker;
beforeEach(() => {
blocker = new SubmitBlocker();
});
it('should allow execution initially', () => {
expect(blocker.canExecute()).toBe(true);
});
it('should block execution after block() is called', () => {
blocker.block();
expect(blocker.canExecute()).toBe(false);
});
it('should allow execution again after release() is called', () => {
blocker.block();
expect(blocker.canExecute()).toBe(false);
blocker.release();
expect(blocker.canExecute()).toBe(true);
});
it('should handle multiple block/release cycles', () => {
blocker.block();
expect(blocker.canExecute()).toBe(false);
blocker.release();
expect(blocker.canExecute()).toBe(true);
blocker.block();
expect(blocker.canExecute()).toBe(false);
blocker.release();
expect(blocker.canExecute()).toBe(true);
});
});

View File

@@ -0,0 +1,22 @@
import { Blocker } from './Blocker';
/**
* SubmitBlocker prevents multiple submissions until explicitly released.
*
* Useful for preventing duplicate form submissions or API calls.
*/
export class SubmitBlocker extends Blocker {
private blocked = false;
canExecute(): boolean {
return !this.blocked;
}
block(): void {
this.blocked = true;
}
release(): void {
this.blocked = false;
}
}

View File

@@ -0,0 +1,59 @@
import { describe, it, expect, vi } from 'vitest';
import { ThrottleBlocker } from './ThrottleBlocker';
describe('ThrottleBlocker', () => {
let blocker: ThrottleBlocker;
beforeEach(() => {
vi.useFakeTimers();
blocker = new ThrottleBlocker(100); // 100ms delay
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should allow execution initially', () => {
expect(blocker.canExecute()).toBe(true);
});
it('should block execution immediately after block() is called', () => {
blocker.block();
expect(blocker.canExecute()).toBe(false);
});
it('should allow execution again after the delay has passed', () => {
blocker.block();
expect(blocker.canExecute()).toBe(false);
// Advance time by 100ms
vi.advanceTimersByTime(100);
expect(blocker.canExecute()).toBe(true);
});
it('should handle multiple block calls', () => {
blocker.block();
expect(blocker.canExecute()).toBe(false);
vi.advanceTimersByTime(50);
expect(blocker.canExecute()).toBe(false); // Still blocked
vi.advanceTimersByTime(50);
expect(blocker.canExecute()).toBe(true);
blocker.block(); // Block again
expect(blocker.canExecute()).toBe(false);
vi.advanceTimersByTime(100);
expect(blocker.canExecute()).toBe(true);
});
it('should work with different delay values', () => {
const fastBlocker = new ThrottleBlocker(50);
fastBlocker.block();
expect(fastBlocker.canExecute()).toBe(false);
vi.advanceTimersByTime(50);
expect(fastBlocker.canExecute()).toBe(true);
});
});

View File

@@ -0,0 +1,24 @@
import { Blocker } from './Blocker';
/**
* ThrottleBlocker enforces a minimum time delay between executions.
*
* Useful for preventing rapid successive actions, such as button mashing.
*/
export class ThrottleBlocker extends Blocker {
private lastExecutionTime = 0;
constructor(private readonly delayMs: number) {}
canExecute(): boolean {
return Date.now() - this.lastExecutionTime >= this.delayMs;
}
block(): void {
this.lastExecutionTime = Date.now();
}
release(): void {
// No-op for throttle blocker
}
}

View File

@@ -0,0 +1,3 @@
export { Blocker } from './Blocker';
export { SubmitBlocker } from './SubmitBlocker';
export { ThrottleBlocker } from './ThrottleBlocker';