Skip to content

Commit

Permalink
chore(demo-integrations): new tests for possible race condition of an…
Browse files Browse the repository at this point in the history
…gular `MaskitoDirective`
  • Loading branch information
nsbarsukov committed Sep 17, 2024
1 parent 691b4eb commit a69c751
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 12 deletions.
5 changes: 4 additions & 1 deletion projects/demo-integrations/src/support/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import {smartTick} from './smart-tick';
declare global {
namespace Cypress {
interface Chainable<Subject> {
smartTick(durationMs: number, frequencyMs?: number): Chainable<Subject>;
smartTick(
durationMs: number,
options?: Parameters<typeof smartTick>[2],
): Chainable<Subject>;
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions projects/demo-integrations/src/support/commands/smart-tick.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import type {ComponentFixture} from '@angular/core/testing';

export function smartTick<T extends Cypress.PrevSubjectMap<void>[Cypress.PrevSubject]>(
$subject: T,
durationMs: number, // ms
frequencyMs = 100, // ms
{
frequencyMs = 100,
fixture,
}: {
fixture?: ComponentFixture<unknown>;
frequencyMs?: number; // ms
} = {},
): Cypress.Chainable<T> {
const iterations = Math.ceil(durationMs / frequencyMs);
const lastIterationMs = durationMs % frequencyMs || frequencyMs;

for (let i = 1; i <= iterations; i++) {
cy.tick(i === iterations ? lastIterationMs : frequencyMs, {log: false});
cy.tick(i === iterations ? lastIterationMs : frequencyMs, {log: false}).then(
() => fixture?.detectChanges(), // ensure @Input()-properties are changed
);
cy.wait(0, {log: false}); // allow React hooks to process
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,33 +102,89 @@ describe('@maskito/angular | Predicate', () => {
});

it('allows to enter letters in both textfield (active predicate is changed; both are still resolving)', () => {
cy.smartTick(520).then(() => {
fixture.detectChanges(); // ensure predicate is changed to valid one
});
cy.smartTick(520, {fixture});

cy.get('@hidden').focus().type('12abc3').should('have.value', '12abc3');
cy.get('@real').focus().type('12abc3').should('have.value', '12abc3');
});

it('allows to enter letters in both textfield (invalid predicate was resolved AND SKIPPED; valid is still resolving)', () => {
cy.smartTick(520).then(() => {
fixture.detectChanges(); // ensure predicate is changed to valid one
});
cy.smartTick(520, {fixture});
cy.smartTick(500); // invalid predicate was resolved

cy.get('@hidden').focus().type('12abc3').should('have.value', '12abc3');
cy.get('@real').focus().type('12abc3').should('have.value', '12abc3');
});

it('forbids to enter letters only in real textfield (valid and invalid predicates were resolved)', () => {
cy.smartTick(520).then(() => {
fixture.detectChanges(); // ensure predicate is changed to valid one
});
cy.smartTick(520, {fixture});
cy.smartTick(500); // invalid predicate was resolved
cy.smartTick(500); // valid predicate was resolved

cy.get('@hidden').focus().type('12abc3').should('have.value', '12abc3');
cy.get('@real').focus().type('12abc3').should('have.value', '123');
});
});

describe('[maskitoOptions] are changed before long element predicate is resolved', () => {
let fixture!: ComponentFixture<unknown>;
const SWITCH_OPTIONS_TIME = 1_000;
const PREDICATE_RESOLVING_TIME = 2_000;

beforeEach(() => {
@Component({
standalone: true,
imports: [MaskitoDirective],
template: `
<input
[maskito]="options()"
[maskitoElement]="elementPredicate"
/>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestComponent {
private readonly numberOptions = {mask: /^\d+$/};
private readonly engLettersOptions = {mask: /^[a-z]+$/i};
protected options = signal(this.numberOptions);

constructor() {
setTimeout(() => {
this.options.set(this.engLettersOptions);
}, SWITCH_OPTIONS_TIME);
}

protected readonly elementPredicate: MaskitoElementPredicate = async (
element,
) =>
new Promise((resolve) => {
setTimeout(
() => resolve(element as HTMLInputElement),
PREDICATE_RESOLVING_TIME,
);
});
}

cy.clock();
cy.mount(TestComponent).then((res) => {
fixture = res.fixture;
});
});

it('can enter any value before no predicate is resolved', () => {
cy.get('input').focus().type('12abc3').should('have.value', '12abc3');
});

// TODO: uncomment in this PR https://github.com/taiga-family/maskito/pull/1608
it.skip('enabling of the first mask should be skipped if [maskitoOptions] were changed during resolving of element predicate', () => {
cy.smartTick(PREDICATE_RESOLVING_TIME, {fixture}); // predicate is resolved only once for digit cases
cy.get('input').focus().type('12abc3').should('have.value', '12abc3');
});

// TODO: uncomment in this PR https://github.com/taiga-family/maskito/pull/1608
it.skip('only the last mask should be applied if [maskitoOptions] were changed during resolving of element predicates', () => {
cy.smartTick(SWITCH_OPTIONS_TIME + PREDICATE_RESOLVING_TIME, {fixture}); // enough time to resolve element predicated for both cases
cy.get('input').focus().type('12abc3').should('have.value', 'abc');
});
});
});

0 comments on commit a69c751

Please sign in to comment.