From a69c751a834b8a3865702098ba82e793255aa05e Mon Sep 17 00:00:00 2001 From: Nikita Barsukov Date: Tue, 17 Sep 2024 15:07:50 +0300 Subject: [PATCH] chore(demo-integrations): new tests for possible race condition of angular `MaskitoDirective` --- .../src/support/commands/index.ts | 5 +- .../src/support/commands/smart-tick.ts | 14 +++- .../angular-predicate/angular-predicate.cy.ts | 74 ++++++++++++++++--- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/projects/demo-integrations/src/support/commands/index.ts b/projects/demo-integrations/src/support/commands/index.ts index 632289055..fac181423 100644 --- a/projects/demo-integrations/src/support/commands/index.ts +++ b/projects/demo-integrations/src/support/commands/index.ts @@ -5,7 +5,10 @@ import {smartTick} from './smart-tick'; declare global { namespace Cypress { interface Chainable { - smartTick(durationMs: number, frequencyMs?: number): Chainable; + smartTick( + durationMs: number, + options?: Parameters[2], + ): Chainable; } } } diff --git a/projects/demo-integrations/src/support/commands/smart-tick.ts b/projects/demo-integrations/src/support/commands/smart-tick.ts index a5763724e..e2f436ed4 100644 --- a/projects/demo-integrations/src/support/commands/smart-tick.ts +++ b/projects/demo-integrations/src/support/commands/smart-tick.ts @@ -1,13 +1,23 @@ +import type {ComponentFixture} from '@angular/core/testing'; + export function smartTick[Cypress.PrevSubject]>( $subject: T, durationMs: number, // ms - frequencyMs = 100, // ms + { + frequencyMs = 100, + fixture, + }: { + fixture?: ComponentFixture; + frequencyMs?: number; // ms + } = {}, ): Cypress.Chainable { 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 } diff --git a/projects/demo-integrations/src/tests/component-testing/angular-predicate/angular-predicate.cy.ts b/projects/demo-integrations/src/tests/component-testing/angular-predicate/angular-predicate.cy.ts index 7e880ceb5..cb42114a4 100644 --- a/projects/demo-integrations/src/tests/component-testing/angular-predicate/angular-predicate.cy.ts +++ b/projects/demo-integrations/src/tests/component-testing/angular-predicate/angular-predicate.cy.ts @@ -102,18 +102,14 @@ 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'); @@ -121,9 +117,7 @@ describe('@maskito/angular | Predicate', () => { }); 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 @@ -131,4 +125,66 @@ describe('@maskito/angular | Predicate', () => { cy.get('@real').focus().type('12abc3').should('have.value', '123'); }); }); + + describe('[maskitoOptions] are changed before long element predicate is resolved', () => { + let fixture!: ComponentFixture; + const SWITCH_OPTIONS_TIME = 1_000; + const PREDICATE_RESOLVING_TIME = 2_000; + + beforeEach(() => { + @Component({ + standalone: true, + imports: [MaskitoDirective], + template: ` + + `, + 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'); + }); + }); });