import {cleanup, render, waitFor} from '@testing-library/react';
import {AsyncChildrenApp, TestApp, type UnitSetupProps, runAllSuites} from '@wix/unidriver-common/testing';
import {delay} from '@wix/unidriver-common';
import React, {type ChangeEvent} from 'react';
import {jsdomReactUniDriver} from '../jsdomReactUniDriver';
describe('Common Test Suite', () => {
runAllSuites({
setup: async (props: UnitSetupProps) => {
const {container, unmount} = render();
const driver = jsdomReactUniDriver(container);
const tearDown = async () => {
unmount();
cleanup();
};
return {driver, tearDown};
},
});
});
describe('Adapter Specific', () => {
afterEach(() => {
vi.useRealTimers();
cleanup();
});
describe('click', () => {
// TODO: check order of fired events
it('should trigger button events when clicked', async () => {
const onMouseUp = vi.fn();
const onClick = vi.fn();
const onMouseDown = vi.fn();
const onFocus = vi.fn();
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
await driver.$('button').click();
expect(onClick).toHaveBeenCalledTimes(1);
expect(onMouseUp).toHaveBeenCalledTimes(1);
expect(onMouseDown).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledTimes(1);
});
it('should trigger input events in order', async () => {
const onMouseUp = vi.fn();
const onClick = vi.fn();
const onMouseDown = vi.fn();
const onFocus = vi.fn();
const onInput = vi.fn();
const onChange = vi.fn();
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
await driver.$('input').click();
expect(onClick).toHaveBeenCalledTimes(1);
expect(onMouseUp).toHaveBeenCalledTimes(1);
expect(onMouseDown).toHaveBeenCalledTimes(1);
expect(onFocus).toHaveBeenCalledTimes(1);
expect(onInput).toHaveBeenCalledTimes(1);
expect(onChange).toHaveBeenCalledTimes(1);
});
it('should trigger focus and blur in order when clicking two buttons', async () => {
const onFocusA = vi.fn();
const onBlurA = vi.fn();
const onFocusB = vi.fn();
const {container} = render(
<>
>,
);
const driver = jsdomReactUniDriver(container);
await driver.$('#A').click();
expect(onFocusA).toHaveBeenCalledTimes(1);
expect(onBlurA).toHaveBeenCalledTimes(0);
expect(onFocusB).toHaveBeenCalledTimes(0);
await driver.$('#B').click();
expect(onBlurA).toHaveBeenCalledTimes(1);
expect(onFocusB).toHaveBeenCalledTimes(1);
});
it('should not call onFocus when given disabled button', async () => {
const onFocusA = vi.fn();
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
await driver.$('button#A').click();
expect(onFocusA).toHaveBeenCalledTimes(0);
});
it('should work with fake timers', async () => {
vi.useFakeTimers();
const {container} = render();
const driver = jsdomReactUniDriver(container);
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
expect(await driver.$('button').click()).toBe(undefined);
});
it(`should trigger double click if {type: 'double'} is passed`, async () => {
const onDoubleClick = vi.fn();
const {container} = render();
const driver = jsdomReactUniDriver(container);
await driver.$('input').click({type: 'double'});
expect(onDoubleClick).toHaveBeenCalledTimes(1);
});
it('should work with pointer-events: none', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
// eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
expect(await driver.$('button').click()).toBe(undefined);
});
});
describe('enterText', () => {
it('should change input value when entering', async () => {
const onChange = vi.fn();
const {container} = render(
{
onChange(event.target.value);
}}
/>,
);
const driver = jsdomReactUniDriver(container);
await driver.$('input').enterText('some keywords');
const firstLetter: string = onChange.mock.calls.at(0)?.[0];
expect(firstLetter).toBe('s');
const value: string = onChange.mock.calls.at(-1)?.[0];
expect(value).toBe('some keywords');
});
it('should change input value when given uncontrolled input', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const input = driver.$('input');
await input.enterText('some keywords');
expect(await input.value()).toBe('some keywords');
});
it('should type with a delay if delay prop provided', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const input = driver.$('input');
const inputPromise = input.enterText('some keywords', {delay: 100});
const assertionPromise = delay(300).then(async () => {
const value = await driver.$('input').value();
expect(value.length).toBeGreaterThan(0);
expect(value.length).toBeLessThan('some keywords'.length);
});
await inputPromise;
await assertionPromise;
expect(await input.value()).toBe('some keywords');
});
});
describe('enterValue', () => {
// This is actually incorrect behaviour and the reason, why .enterValue() is deprecated
it('should change input value when entering with single change', async () => {
const onChange = vi.fn();
const {container} = render();
const driver = jsdomReactUniDriver(container);
await driver.$('input').enterValue('some keywords');
const event: ChangeEvent = onChange.mock.calls.at(0)?.[0];
expect(event.target.value).toEqual('some keywords');
expect(onChange.mock.calls.length).toBe(1);
});
it('should change input value when given uncontrolled input', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const input = driver.$('input');
await input.enterValue('some keywords');
expect(await input.value()).toBe('some keywords');
});
it('should type with a delay if delay prop provided', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const input = driver.$('input');
const inputPromise = input.enterValue('some keywords', {delay: 100});
const assertionPromise = delay(300).then(async () => {
const value = await driver.$('input').value();
expect(value.length).toBeGreaterThan(0);
expect(value.length).toBeLessThan('some keywords'.length);
});
await inputPromise;
await assertionPromise;
expect(await input.value()).toBe('some keywords');
});
});
describe('wait', () => {
it('should throw immediately if element is not found when passed zero timeout', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const firstCompletedTask = await Promise.race([
driver
.$('button')
.wait(0)
.catch(() => {
return 'wait';
}),
new Promise((resolve) =>
setTimeout(() => {
resolve('timeout');
}, 100),
),
]);
expect(firstCompletedTask).toBe('wait');
});
it('should resolve promise only after the element is in the DOM', async () => {
const {container, rerender} = render();
const driver = jsdomReactUniDriver(container);
const selector = '[data-testid="test-element"]';
expect(await driver.$(selector).exists()).toBe(false);
const waitPromise = driver.$(selector).wait();
rerender();
expect(await driver.$(selector).exists()).toBe(true);
await expect(waitPromise).resolves.toBe(undefined);
});
it('should reject promise if element does not appear in the DOM', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
await expect(driver.$('button').wait()).rejects.toThrowError();
});
});
describe('$', () => {
describe('awaited', () => {
it('should have basic support', async () => {
const onClick = vi.fn();
const {container} = render();
const driver = jsdomReactUniDriver(container);
const task = await driver
.$('button')
.awaited()
.click()
.then(() => 'click');
expect(task).toBe('click');
expect(onClick).toHaveBeenCalledTimes(1);
});
it('should wait timeout for element to show if applied to deep driver', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const firstCompletedTask = await Promise.race([
driver
.$('div')
.awaited(100)
.$('button')
.click()
.catch(() => {
return 'selector';
}),
new Promise((resolve) => {
setTimeout(() => {
resolve('timeout');
}, 200);
}),
]);
expect(firstCompletedTask).toBe('selector');
});
it('should override timeout', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const firstCompletedTask = await Promise.race([
driver
.$('div')
.awaited(300)
.$('button')
.awaited(100)
.click()
.catch(() => {
return 'selector';
}),
new Promise((resolve) => {
setTimeout(() => {
resolve('timeout');
}, 200);
}),
]);
expect(firstCompletedTask).toBe('selector');
});
it('should show full selectors path if element not present', async () => {
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
const clickTaskError = await driver
.$('div')
.$('section')
.$('span')
.$('div')
.$('button')
.awaited(10)
.click()
.catch((e: unknown) => {
return (e as Error).message;
});
expect(clickTaskError).toContain('selector: button\r\n');
expect(clickTaskError).toContain('Failed path: div > section > span\r\n');
expect(clickTaskError).toContain('Full path: div > section > span > div > button');
});
});
it('should not throw immediately', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const completedTask = await Promise.race([
driver
.$('button')
.click()
.catch(() => {
return 'selector';
}),
new Promise((resolve) => {
setTimeout(() => {
resolve('timeout');
}, 100);
}),
]);
expect(completedTask).toBe('timeout');
});
it('should show full selectors path if element not present', async () => {
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
const clickTaskError = await driver
.$('div')
.$('section')
.$('span')
.$('div')
.$('button')
.awaited(10)
.click()
.catch((e: unknown) => {
return (e as Error).message;
});
expect(clickTaskError).toContain('selector: button\r\n');
expect(clickTaskError).toContain('Failed path: div > section > span\r\n');
expect(clickTaskError).toContain('Full path: div > section > span > div > button');
});
it('should be able to get an element when app renders element with a delay', async () => {
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
await waitFor(async () => {
expect(await driver.$('[data-testid="delayed-element"]').exists()).toBe(true);
});
});
it('should return an object which syncs with the DOM', async () => {
const {container, rerender} = render();
const driver = jsdomReactUniDriver(container);
const element = driver.$('[data-testid="test-element"]');
expect(await element.exists()).toBe(true);
rerender();
expect(await element.exists()).toBe(false);
});
});
describe('$$', () => {
it('should return unidriver list', async () => {
const {container} = render(
<>
>,
);
const driver = jsdomReactUniDriver(container);
const elementList = driver.$$('[data-testid="test-element"]');
expect(await elementList.get(0).exists()).toBe(true);
expect(await elementList.get(1).exists()).toBe(true);
expect(await elementList.get(2).exists()).toBe(false);
});
it('should show full selectors path if element not present', async () => {
const {container} = render(
<>
>,
);
const driver = jsdomReactUniDriver(container);
const clickTaskError = await driver
.$('div')
.$$('section')
.at(0)
.$('span')
.$('div')
.$('button')
.awaited(10)
.click()
.catch((e: unknown) => {
return (e as Error).message;
});
expect(clickTaskError).toContain('selector: button\r\n');
expect(clickTaskError).toContain('Failed path: div > section:nth-child(1) > span\r\n');
expect(clickTaskError).toContain('Full path: div > section:nth-child(1) > span > div > button');
});
it('should be able to get an element(s) when app renders element with a delay', async () => {
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
expect(await driver.$('[data-testid="delayed-element"]').exists()).toBe(false);
const elementList = driver.$$('[data-testid="delayed-element"]');
await waitFor(async () => {
expect(await elementList.get(0).exists()).toBe(true);
expect(await elementList.get(1).exists()).toBe(true);
});
});
it(`should sync with the DOM when it's updated`, async () => {
const {container, rerender} = render(
,
);
const driver = jsdomReactUniDriver(container);
const elementList = driver.$$('[data-testid="test-element"]');
expect(await elementList.get(0).exists()).toBe(true);
expect(await elementList.get(1).exists()).toBe(true);
rerender();
expect(await elementList.get(0).exists()).toBe(false);
expect(await elementList.get(1).exists()).toBe(false);
});
});
describe('get', () => {
it('should resolve a promise with a unidriver element', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
const element = await driver.get('[data-testid="test-element"]');
expect(await element.exists()).toBe(true);
});
it('should be able to get an element when app renders element with a delay', async () => {
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
expect(await driver.$('[data-testid="delayed-element"]').exists()).toBe(false);
const element = await driver.get('[data-testid="delayed-element"]');
expect(await element.exists()).toBe(true);
});
it('should throw error when element is not found', async () => {
const {container} = render();
const driver = jsdomReactUniDriver(container);
let error: unknown;
try {
await driver.get('[data-testid="test-element"]', {
timeout: 10,
});
} catch (e) {
error = e;
}
expect(error).toBeInstanceOf(Error);
});
it('should return an object which syncs with the DOM', async () => {
const {container, rerender} = render();
const driver = jsdomReactUniDriver(container);
const element = await driver.get('[data-testid="test-element"]');
expect(await element.exists()).toBe(true);
rerender();
expect(await element.exists()).toBe(false);
});
});
describe('getAll', () => {
it('should resolve a promise with a unidriver element list', async () => {
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
const elementList = await driver.getAll('[data-testid="test-element"]');
expect(await elementList.at(0).exists()).toBe(true);
expect(await elementList.at(1).exists()).toBe(true);
expect(await elementList.at(2).exists()).toBe(false);
});
it('should be able to get an element(s) when app renders element with a delay', async () => {
const {container} = render(
,
);
const driver = jsdomReactUniDriver(container);
expect(await driver.$('[data-testid="delayed-element"]').exists()).toBe(false);
const elementList = await driver.getAll('[data-testid="delayed-element"]');
expect(await elementList.at(0).exists()).toBe(true);
expect(await elementList.at(1).exists()).toBe(true);
});
it('should throw error when element is not found', async () => {
vi.useFakeTimers();
const {container} = render();
const driver = jsdomReactUniDriver(container);
const getterPromise = driver.getAll('[data-testid="test-element"]');
await expect(getterPromise).rejects.toThrowError();
});
it('should return an object which syncs with the DOM', async () => {
const {container, rerender} = render(
,
);
const driver = jsdomReactUniDriver(container);
const elementList = await driver.getAll('[data-testid="test-element"]');
expect(await elementList.at(0).exists()).toBe(true);
expect(await elementList.at(1).exists()).toBe(true);
rerender();
expect(await elementList.at(0).exists()).toBe(false);
expect(await elementList.at(1).exists()).toBe(false);
});
});
it('getNative', async () => {
const {container} = render(
test
,
);
const driver = jsdomReactUniDriver(container);
const span = driver.$('span');
const native: HTMLSpanElement = await span.getNative();
expect(native.textContent).toBe('test');
});
});