Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions cypress/e2e/perf-resize-throttle.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/** cSpell:ignore vscomp */

// Tests for P1: the global window 'resize' handler used to run per-instance height
// recomputation on every resize tick, unthrottled, and threw if a wrapper had no instance.

describe('Performance: window resize handler (P1)', () => {
const mountId = 'vs-p1-resize';

const mount = (win: Window) => {
const doc = win.document;
const existing = doc.getElementById(mountId);
if (existing) {
existing.remove();
}
const $ele = doc.createElement('div');
$ele.id = mountId;
doc.body.appendChild($ele);
// @ts-expect-error - VirtualSelect attached to window by the bundle
win.VirtualSelect.init({ ele: $ele, options: [{ label: 'A', value: 'a' }, { label: 'B', value: 'b' }] });
return $ele;
};

it('throttles resize so onResize runs far fewer times than resize events fire', () => {
cy.visit('get-started');
cy.window().then((win) => {
mount(win);
// @ts-expect-error - instance back-reference
const instance = win.document.getElementById(mountId).virtualSelect;
cy.spy(instance, 'onResize').as('onResize');

for (let i = 0; i < 10; i += 1) {
win.dispatchEvent(new win.Event('resize'));
}
});

// allow the trailing-edge call to fire
cy.wait(250);
cy.get('@onResize').its('callCount').should('be.greaterThan', 0).and('be.lessThan', 10);
});

it('still updates the dropdown on resize (no regression)', () => {
cy.visit('get-started');
cy.window().then((win) => mount(win));

cy.get(`#${mountId}`).find('.vscomp-toggle-button').click();
cy.get(`#${mountId}`).find('.vscomp-option').should('have.length.greaterThan', 0);

cy.window().then((win) => win.dispatchEvent(new win.Event('resize')));
cy.wait(150);

// dropdown remains open and functional after a resize
cy.get(`#${mountId}`).find('.vscomp-ele-wrapper').should('not.have.class', 'closed');
cy.get(`#${mountId}`).find('.vscomp-options-container').should('exist');
});

it('does not throw when a wrapper has no associated instance (guard)', () => {
cy.visit('get-started');
cy.window().then((win) => {
const doc = win.document;
const orphan = doc.createElement('div');
orphan.innerHTML = '<div class="vscomp-ele-wrapper"></div>'; // parent has no .virtualSelect
doc.body.appendChild(orphan);

// would throw "Cannot read properties of undefined (reading 'onResize')" without the guard
win.dispatchEvent(new win.Event('resize'));
});

// if the handler threw, Cypress would have failed the test on the uncaught exception
cy.get('body').should('exist');
});
});
69 changes: 69 additions & 0 deletions cypress/e2e/timer-cleanup.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/** cSpell:ignore vscomp */

// Tests for P4: pending setTimeout callbacks were not cleared on destroy(), so they could run
// against a destroyed instance. Timers now go through setManagedTimeout and are cleared in destroy().

describe('Hardening: managed timeouts cleared on destroy (P4)', () => {
const mountId = 'vs-p4-timers';

const mount = (win: Window) => {
const doc = win.document;
const existing = doc.getElementById(mountId);
if (existing) {
existing.remove();
}
const $ele = doc.createElement('div');
$ele.id = mountId;
doc.body.appendChild($ele);
// @ts-expect-error - VirtualSelect attached to window by the bundle
win.VirtualSelect.init({ ele: $ele, options: [{ label: 'A', value: 'a' }, { label: 'B', value: 'b' }] });
return $ele.virtualSelect;
};

it('cancels a pending managed timeout when the instance is destroyed', () => {
cy.visit('get-started');
// Use a synthetic clock so the negative assertion (timer must NOT fire) is deterministic.
// With a real-time cy.wait, CI timer-throttling could delay a still-pending 50ms callback
// past the wait window, making the test pass even if destroy() failed to clear it.
cy.clock();
cy.window().then((win) => {
// @ts-expect-error - test marker
win.__managedTimerFired = false;
const instance = mount(win);
instance.setManagedTimeout(() => {
// @ts-expect-error - test marker
win.__managedTimerFired = true;
}, 50);
instance.destroy();
});

// Advance well past the 50ms timeout; a cleared timer can never fire, a leaked one would.
cy.tick(150);
cy.window().then((win) => {
// @ts-expect-error - test marker
expect(win.__managedTimerFired, 'managed timeout must not fire after destroy').to.eq(false);
});
});

it('does not throw when destroying right after an option select (no regression)', () => {
cy.visit('get-started');
cy.window().then((win) => mount(win));

cy.get(`#${mountId}`).find('.vscomp-toggle-button').click();
cy.get(`#${mountId}`).find('.vscomp-option[data-value="a"]').click();

cy.window().then((win) => {
const $ele = win.document.getElementById(mountId);
// @ts-expect-error - instance back-reference
$ele.virtualSelect.destroy();
});

cy.wait(50);
cy.get('body').should('exist');
cy.window().then((win) => {
const $ele = win.document.getElementById(mountId);
// @ts-expect-error - instance back-reference
expect($ele.virtualSelect, 'instance reference cleared on destroy').to.eq(undefined);
});
});
});
Loading